fix(i18n): sync all lesson translations with English source

Synchronizes 72 lesson files across 5 languages (de, pl, es, ar, uk) to match
the English source. This ensures code, solutions, and validations are identical
while only title, description, task, and message fields are translated.

Changes include:
- Box model lessons (01-box-model.json)
- Units and variables (05-units-variables.json)
- Transitions and animations (06-transitions-animations.json)
- Responsive design (08-responsive.json)
- HTML elements (20-html-elements.json)
- HTML forms basic and validation (21, 22)
- HTML details/summary, progress/meter (23, 24)
- HTML datalist, dialog, fieldset (25, 27, 28)
- HTML tables and SVG (30, 32)
- HTML marquee (31)
- Welcome module (00-welcome.json)

Fixes validation inconsistencies and removes extra content that exceeded
English source. German translations were largely correct; Polish, Spanish,
Arabic, and Ukrainian required full translations.
This commit is contained in:
2026-01-14 15:39:22 +01:00
parent cc2faa5104
commit 4bed75eb74
72 changed files with 2206 additions and 2611 deletions

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "welcome", "id": "welcome",
"title": "Code Crispies", "title": "Code Crispies",
"description": "Welcome to Code Crispies - your interactive web development learning platform", "description": "مرحباً بك في Code Crispies - منصتك التفاعلية لتعلم تطوير الويب",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "get-started", "id": "get-started",
"title": "Get Started", "title": "ابدأ",
"description": "<strong>Code Crispies</strong> is a free, open-source platform for learning web development through hands-on exercises. No account required!<br><br><strong>What you'll learn:</strong><br>• <strong>HTML</strong> - Semantic elements, forms, tables, SVG (<em>HTML Block & Inline</em>, <em>HTML Forms</em>, <em>HTML Tables</em>)<br>• <strong>CSS</strong> - Selectors, box model, flexbox, animations (<em>CSS Selectors</em>, <em>CSS Box Model</em>, <em>CSS Flexbox</em>)<br>• <strong>Responsive Design</strong> - Media queries and mobile-first layouts<br><br><strong>How it works:</strong><br>1. Read the task in the left panel<br>2. Write code in the editor<br>3. See live results in the preview<br>4. Get instant feedback with hints<br><br><strong>Keyboard shortcuts:</strong> <kbd>Ctrl+Z</kbd> to undo, <kbd>Ctrl+Shift+Z</kbd> to redo<br><br><strong>More resources:</strong><br>• <a href=\"https://nextlevelshit.github.io/html-over-js/\" target=\"_blank\">HTML over JS</a> - Native HTML vs JavaScript solutions<br>• <a href=\"https://nextlevelshit.github.io/web-engineering-mandala/\" target=\"_blank\">Web Engineering Mandala</a> - JavaScript technology roadmap", "description": "<strong>Code Crispies</strong> منصة مجانية ومفتوحة المصدر لتعلم تطوير الويب من خلال التمارين العملية. لا حاجة لحساب!<br><br><strong>ما ستتعلمه:</strong><br>• <strong>HTML</strong> - العناصر الدلالية، النماذج، الجداول، SVG (<em>HTML كتلي وسطري</em>، <em>HTML النماذج</em>، <em>HTML الجداول</em>)<br>• <strong>CSS</strong> - المحددات، نموذج الصندوق، flexbox، الحركات (<em>CSS المحددات</em>، <em>CSS نموذج الصندوق</em>، <em>CSS Flexbox</em>)<br>• <strong>التصميم المتجاوب</strong> - استعلامات الوسائط وتخطيطات mobile-first<br><br><strong>كيف يعمل:</strong><br>1. اقرأ المهمة في اللوحة اليسرى<br>2. اكتب الكود في المحرر<br>3. شاهد النتائج مباشرة في المعاينة<br>4. احصل على ملاحظات فورية مع تلميحات<br><br><strong>اختصارات لوحة المفاتيح:</strong> <kbd>Ctrl+Z</kbd> تراجع، <kbd>Ctrl+Shift+Z</kbd> إعادة<br><br><strong>المزيد من الموارد:</strong><br>• <a href=\"https://nextlevelshit.github.io/html-over-js/\" target=\"_blank\">HTML over JS</a> - HTML الأصلي مقابل حلول JavaScript<br>• <a href=\"https://nextlevelshit.github.io/web-engineering-mandala/\" target=\"_blank\">Web Engineering Mandala</a> - خارطة تقنيات JavaScript",
"task": "Write <code>Hello World</code>", "task": "اكتب <code>Hello World</code>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; font-size: 2rem; color: #6366f1; font-weight: bold; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; font-size: 2rem; color: #6366f1; font-weight: bold; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,15 +21,15 @@
{ {
"type": "contains", "type": "contains",
"value": "Hello World", "value": "Hello World",
"message": "Write <code>Hello World</code>" "message": "اكتب <code>Hello World</code>"
} }
] ]
}, },
{ {
"id": "overview", "id": "overview",
"title": "Overview", "title": "نظرة عامة",
"description": "<strong>You're ready!</strong> Open the menu (☰) to explore all modules.<br><br><strong>Recommended learning path:</strong><br>1. <em>HTML Block & Inline</em> - Understand container vs inline elements<br>2. <em>HTML Forms</em> - Build interactive forms with validation<br>3. <em>CSS Selectors</em> - Target elements precisely<br>4. <em>CSS Box Model</em> - Master padding, margin, borders<br>5. <em>CSS Flexbox</em> - Create flexible layouts<br>6. <em>CSS Animations</em> - Add motion and transitions<br><br><strong>Tips:</strong><br>• Use <em>Show Expected</em> to see the target result<br>• Your progress is saved automatically<br>• Try Emmet in HTML mode: <kbd>ul>li*3</kbd> + Tab<br><br><strong>Open Source:</strong><br>• <a href=\"https://git.librete.ch/public/code-crispies\" target=\"_blank\">Gitea (Source)</a> · <a href=\"https://github.com/nextlevelshit/code-crispies\" target=\"_blank\">GitHub (Mirror)</a><br>• Made by <a href=\"https://librete.ch\" target=\"_blank\">LibreTECH</a> · <a href=\"https://www.linkedin.com/in/michael-werner-czechowski\" target=\"_blank\">Michael Czechowski</a>", "description": "<strong>أنت جاهز!</strong> افتح القائمة (☰) لاستكشاف جميع الوحدات.<br><br><strong>مسار التعلم الموصى به:</strong><br>1. <em>HTML كتلي وسطري</em> - افهم عناصر الحاوية مقابل السطرية<br>2. <em>HTML النماذج</em> - أنشئ نماذج تفاعلية مع التحقق<br>3. <em>CSS المحددات</em> - استهدف العناصر بدقة<br>4. <em>CSS نموذج الصندوق</em> - أتقن padding، margin، borders<br>5. <em>CSS Flexbox</em> - أنشئ تخطيطات مرنة<br>6. <em>CSS الحركات</em> - أضف الحركة والانتقالات<br><br><strong>نصائح:</strong><br>• استخدم <em>إظهار المتوقع</em> لرؤية النتيجة المستهدفة<br>• يُحفظ تقدمك تلقائياً<br>• جرب Emmet في وضع HTML: <kbd>ul>li*3</kbd> + Tab<br><br><strong>مفتوح المصدر:</strong><br>• <a href=\"https://git.librete.ch/public/code-crispies\" target=\"_blank\">Gitea (Source)</a> · <a href=\"https://github.com/nextlevelshit/code-crispies\" target=\"_blank\">GitHub (Mirror)</a><br>• صُنع بواسطة <a href=\"https://librete.ch\" target=\"_blank\">LibreTECH</a> · <a href=\"https://www.linkedin.com/in/michael-werner-czechowski\" target=\"_blank\">Michael Czechowski</a>",
"task": "Click Next to continue", "task": "انقر التالي للمتابعة",
"previewHTML": "<p>Hello World! 🌍</p><p>Hallo Welt!</p><p>Bonjour le monde!</p><p>¡Hola Mundo!</p><p>Ciao Mondo!</p><p>Olá Mundo!</p><p>こんにちは世界!</p><p>你好世界!</p><p>안녕 세상!</p><p>Привет мир!</p><p dir=\"rtl\">שלום עולם!</p><p dir=\"rtl\">مرحبا بالعالم!</p>", "previewHTML": "<p>Hello World! 🌍</p><p>Hallo Welt!</p><p>Bonjour le monde!</p><p>¡Hola Mundo!</p><p>Ciao Mondo!</p><p>Olá Mundo!</p><p>こんにちは世界!</p><p>你好世界!</p><p>안녕 세상!</p><p>Привет мир!</p><p dir=\"rtl\">שלום עולם!</p><p dir=\"rtl\">مرحبا بالعالم!</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 12px; } p { margin: 6px 0; padding: 6px 12px; border-radius: 6px; font-weight: 600; font-size: 0.95em; } p:nth-child(1) { background: #fef3c7; color: #92400e; font-size: 1.2em; } p:nth-child(2) { background: #dcfce7; color: #166534; } p:nth-child(3) { background: #dbeafe; color: #1e40af; } p:nth-child(4) { background: #fce7f3; color: #9d174d; } p:nth-child(5) { background: #e0e7ff; color: #4338ca; } p:nth-child(6) { background: #fef9c3; color: #854d0e; } p:nth-child(7) { background: #fee2e2; color: #991b1b; } p:nth-child(8) { background: #f3e8ff; color: #7c3aed; } p:nth-child(9) { background: #ccfbf1; color: #0f766e; } p:nth-child(10) { background: #fae8ff; color: #86198f; } p:nth-child(11) { background: #fef3c7; color: #b45309; } p:nth-child(12) { background: #d1fae5; color: #047857; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 12px; } p { margin: 6px 0; padding: 6px 12px; border-radius: 6px; font-weight: 600; font-size: 0.95em; } p:nth-child(1) { background: #fef3c7; color: #92400e; font-size: 1.2em; } p:nth-child(2) { background: #dcfce7; color: #166534; } p:nth-child(3) { background: #dbeafe; color: #1e40af; } p:nth-child(4) { background: #fce7f3; color: #9d174d; } p:nth-child(5) { background: #e0e7ff; color: #4338ca; } p:nth-child(6) { background: #fef9c3; color: #854d0e; } p:nth-child(7) { background: #fee2e2; color: #991b1b; } p:nth-child(8) { background: #f3e8ff; color: #7c3aed; } p:nth-child(9) { background: #ccfbf1; color: #0f766e; } p:nth-child(10) { background: #fae8ff; color: #86198f; } p:nth-child(11) { background: #fef3c7; color: #b45309; } p:nth-child(12) { background: #d1fae5; color: #047857; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -39,8 +39,8 @@
"validations": [ "validations": [
{ {
"type": "contains", "type": "contains",
"value": "Hello World", "value": "Hello",
"message": "Click Next to continue" "message": "انقر التالي للمتابعة"
} }
] ]
}, },

View File

@@ -2,18 +2,18 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "box-model", "id": "box-model",
"title": "CSS Box Model", "title": "CSS Box Model",
"description": "Master the fundamental principles of space management in web design through the CSS box model. This module explores how content, padding, borders, and margins combine to create layout structures that are both visually appealing and structurally sound.", "description": "أتقن المبادئ الأساسية لإدارة المساحة في تصميم الويب من خلال نموذج الصندوق CSS. يستكشف هذا الوحدة كيف يتحد المحتوى والحشو والحدود والهوامش لإنشاء هياكل التخطيط.",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "box-model-1", "id": "box-model-1",
"title": "Box Model Components", "title": "Padding",
"description": "The CSS box model consists of four concentric layers: content area (innermost), padding, border, and margin (outermost). Understanding how these components interact is essential for precise layout control.", "description": "كل عنصر في CSS هو صندوق بأربع طبقات: المحتوى، الحشو (padding)، الحدود، والهامش. <strong>Padding</strong> يخلق مساحة تنفس بين محتواك وحافة الصندوق.<br><br>بدون padding، يضغط النص بشكل محرج على الحدود. Padding يجعل المحتوى قابلاً للقراءة ومتوازناً بصرياً.<br><br><pre>.card {\n padding: 1rem;\n}</pre>",
"task": "Set <kbd>padding</kbd> to <kbd>1rem</kbd> to create space between the content and border.", "task": "بطاقة الملف الشخصي هذه تبدو ضيقة. أضف <kbd>padding: 1rem</kbd> ليكون للنص مجال للتنفس.",
"previewHTML": "<div class=\"box\">Box Model Components</div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: lavender; border: 2px dashed slategray; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".box {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "padding: 1rem;", "solution": "padding: 1rem;",
@@ -22,62 +22,62 @@
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "padding", "expected": "1rem" }, "value": { "property": "padding", "expected": "1rem" },
"message": "Set <kbd>padding: 1rem</kbd>" "message": "اضبط <kbd>padding: 1rem</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-2", "id": "box-model-2",
"title": "Adding Borders", "title": "Borders",
"description": "Borders outline an element, creating visual separation from surrounding content. The border shorthand accepts three values: width, style, and color.", "description": "الحدود تنشئ حدوداً مرئية حول العناصر. اختصار <kbd>border</kbd> يقبل ثلاث قيم: العرض، النمط، واللون.<br><br>الأنماط الشائعة: <kbd>solid</kbd>، <kbd>dashed</kbd>، <kbd>dotted</kbd>، <kbd>none</kbd>",
"task": "Set <kbd>border</kbd> to <kbd>2px solid darkslategray</kbd>.", "task": "أضف لمسة يسارية خفيفة للبطاقة باستخدام <kbd>border-left: 4px solid steelblue</kbd>.",
"previewHTML": "<div class=\"box\">This box needs a border</div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: mintcream; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".box {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "border: 2px solid darkslategray;", "solution": "border-left: 4px solid steelblue;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "regex", "type": "regex",
"value": "border:\\s*2px\\s+solid\\s+darkslategray", "value": "border-left:\\s*4px\\s+solid\\s+steelblue",
"message": "Set <kbd>border: 2px solid darkslategray</kbd>", "message": "اضبط <kbd>border-left: 4px solid steelblue</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "box-model-3", "id": "box-model-3",
"title": "Adding Margins", "title": "Margins",
"description": "Margins create space between elements, controlling how they relate to one another within a layout. Unlike padding (which affects internal spacing), margins exist outside the element's border.", "description": "الهوامش تنشئ مساحة <em>خارج</em> العنصر، تفصله عن جيرانه. بينما يدفع padding المحتوى للداخل، الهوامش تدفع العناصر الأخرى بعيداً.",
"task": "Set <kbd>margin</kbd> to <kbd>1rem</kbd> to create space between this element and its neighbors.", "task": "أضف مساحة بين بطاقتي الملف الشخصي هاتين باستخدام <kbd>margin-bottom: 1rem</kbd> على <kbd>.card</kbd>.",
"previewHTML": "<div class=\"container\"><div class=\"outer\">This box needs margins</div><div class=\"neighbor\">Adjacent element</div></div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article><article class=\"card\"><h3>Alex Rivera</h3><p>UX Designer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .outer { background-color: plum; padding: 1rem; border: 2px solid orchid; } .neighbor { background-color: lightblue; padding: 1rem; border: 2px solid steelblue; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".outer {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "margin: 1rem;", "solution": "margin-bottom: 1rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "margin", "expected": "1rem" }, "value": { "property": "margin-bottom", "expected": "1rem" },
"message": "Set <kbd>margin: 1rem</kbd>" "message": "اضبط <kbd>margin-bottom: 1rem</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-4", "id": "box-model-4",
"title": "Box Sizing: Border-Box", "title": "Box Sizing",
"description": "The <kbd>box-sizing</kbd> property determines how element dimensions are calculated. The default <kbd>content-box</kbd> excludes padding and border from width/height, while <kbd>border-box</kbd> includes them, making layout calculations more intuitive.", "description": "افتراضياً، <kbd>width</kbd> يحدد فقط عرض المحتوى. Padding والحدود تُضاف للمجموع. هذا يسبب مشاكل في التخطيط.<br><br><kbd>box-sizing: border-box</kbd> يشمل padding والحدود في العرض، مما يجعل التحجيم متوقعاً. معظم المطورين يطبقون هذا على جميع العناصر.",
"task": "Set <kbd>box-sizing</kbd> to <kbd>border-box</kbd> so padding and border are included in the width.", "task": "كلا البطاقتين لهما <kbd>width: 200px</kbd>. اليسرى تستخدم التحجيم الافتراضي (content-box)، مما يجعلها أعرض من المتوقع. أصلح البطاقة اليمنى باستخدام <kbd>box-sizing: border-box</kbd>.",
"previewHTML": "<div class=\"sizing-demo\"><div class=\"box default\">Content-box (default)</div><div class=\"box sized\">Border-box</div></div>", "previewHTML": "<div class=\"demo\"><article class=\"card\">Content-box</article><article class=\"card fix\">Border-box</article></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .sizing-demo { display: flex; gap: 1rem; } .box { width: 200px; padding: 1rem; border: 4px solid teal; background: lightcyan; } .default { box-sizing: content-box; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .demo { display: flex; gap: 1rem; } .card { width: 200px; padding: 1rem; border: 4px solid steelblue; background: white; border-radius: 8px; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".sized {\n ", "codePrefix": ".fix {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "box-sizing: border-box;", "solution": "box-sizing: border-box;",
@@ -86,93 +86,104 @@
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "box-sizing", "expected": "border-box" }, "value": { "property": "box-sizing", "expected": "border-box" },
"message": "Set <kbd>box-sizing: border-box</kbd>" "message": "اضبط <kbd>box-sizing: border-box</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-5", "id": "box-model-5",
"title": "Margin Collapse", "title": "Padding Shorthand",
"description": "When two vertical margins meet, they collapse to the larger value instead of adding up. Understanding this behavior is crucial for consistent vertical spacing.", "description": "Padding يقبل 1-4 قيم:<br>• قيمة واحدة: جميع الجوانب<br>• قيمتان: عمودي | أفقي<br>• 4 قيم: أعلى | يمين | أسفل | يسار",
"task": "Set <kbd>margin-bottom</kbd> to <kbd>2rem</kbd>. Notice the space between paragraphs equals 2rem (not 3rem) due to margin collapse.", "task": "هذا الزر يحتاج مساحة أفقية أكثر من العمودية. اضبط <kbd>padding: 8px 1rem</kbd> (8px أعلى/أسفل، 1rem يسار/يمين).",
"previewHTML": "<div class=\"collapse-demo\"><p class=\"first\">This paragraph has a bottom margin.</p><p class=\"second\">This paragraph has a top margin of 1rem.</p></div>", "previewHTML": "<button class=\"btn\">Follow</button>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .collapse-demo { border: 1px solid silver; padding: 8px; background: ghostwhite; } .second { margin-top: 1rem; background: linen; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".first {\n ", "codePrefix": ".btn {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "margin-bottom: 2rem;", "solution": "padding: 8px 1rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "regex",
"value": { "property": "margin-bottom", "expected": "2rem" }, "value": "padding:\\s*8px\\s+1rem",
"message": "Set <kbd>margin-bottom: 2rem</kbd>" "message": "اضبط <kbd>padding: 8px 1rem</kbd>",
"options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "box-model-6", "id": "box-model-6",
"title": "Margin Shorthand Notation", "title": "Margin Shorthand",
"description": "The margin shorthand can set all four sides at once. Two values set vertical (top/bottom) and horizontal (left/right) margins respectively.", "description": "Margin يستخدم نفس نمط الاختصار مثل padding. نمط شائع هو توسيط عناصر الكتلة أفقياً باستخدام <kbd>margin: 0 auto</kbd>.",
"task": "Set <kbd>margin</kbd> to <kbd>1rem 2rem</kbd> for 1rem top/bottom and 2rem left/right.", "task": "وسّط هذه البطاقة أفقياً. اضبط <kbd>margin: 0 auto</kbd> لحساب هوامش يسار/يمين متساوية تلقائياً.",
"previewHTML": "<div class=\"container\"><div class=\"spaced\">This box needs margins: 1rem top/bottom, 2rem left/right</div></div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .spaced { background-color: honeydew; border: 2px solid mediumseagreen; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { width: 250px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".spaced {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "margin: 1rem 2rem;", "solution": "margin: 0 auto;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "regex", "type": "regex",
"value": "margin:\\s*1rem\\s+2rem", "value": "margin:\\s*0\\s+auto",
"message": "Set <kbd>margin: 1rem 2rem</kbd>", "message": "اضبط <kbd>margin: 0 auto</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "box-model-7", "id": "box-model-7",
"title": "Padding Shorthand Notation", "title": "Border Radius",
"description": "Like margin, padding shorthand allows setting all sides at once. A single value applies to all four sides equally.", "description": "على الرغم من أنه ليس جزءاً من نموذج الصندوق الكلاسيكي، <kbd>border-radius</kbd> يُدوّر زوايا صندوق حدود العنصر. استخدم <kbd>50%</kbd> على عنصر مربع لإنشاء دائرة.",
"task": "Set <kbd>padding</kbd> to <kbd>2rem</kbd> to add equal padding on all sides.", "task": "اجعل صورة الأفاتار دائرية باستخدام <kbd>border-radius: 50%</kbd>.",
"previewHTML": "<div class=\"padded\">This box needs equal padding on all sides</div>", "previewHTML": "<article class=\"card\"><img class=\"avatar\" src=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='35' r='25' fill='%23666'/%3E%3Ccircle cx='50' cy='90' r='40' fill='%23666'/%3E%3C/svg%3E\" alt=\"Avatar\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .padded { background-color: papayawhip; border: 2px solid orange; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; text-align: center; } .avatar { width: 80px; height: 80px; background: #ddd; margin-bottom: 8px; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".padded {\n ", "codePrefix": ".avatar {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "padding: 2rem;", "solution": "border-radius: 50%;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "padding", "expected": "2rem" }, "value": { "property": "border-radius", "expected": "50%" },
"message": "Set <kbd>padding: 2rem</kbd>" "message": "اضبط <kbd>border-radius: 50%</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-8", "id": "box-model-8",
"title": "Border on Specific Sides", "title": "Complete Card",
"description": "For granular control, you can target specific sides with <kbd>border-top</kbd>, <kbd>border-right</kbd>, <kbd>border-bottom</kbd>, or <kbd>border-left</kbd>.", "description": "لنجمع كل شيء معاً. بطاقة الإشعار هذه تحتاج تنسيقاً لتبدو احترافية.",
"task": "Set <kbd>border-bottom</kbd> to <kbd>4px solid dodgerblue</kbd>.", "task": "نسّق الإشعار: أضف <kbd>padding: 1rem</kbd>، <kbd>border-left: 4px solid coral</kbd>، و<kbd>border-radius: 4px</kbd>.",
"previewHTML": "<div class=\"line\">This element needs only a bottom border</div>", "previewHTML": "<div class=\"alert\"><strong>New message</strong><p>You have 3 unread notifications</p></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .line { padding: 1rem; background-color: aliceblue; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { background: seashell; } .alert strong { color: coral; } .alert p { margin: 4px 0 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".line {\n ", "codePrefix": ".alert {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "border-bottom: 4px solid dodgerblue;", "solution": "padding: 1rem;\n border-left: 4px solid coral;\n border-radius: 4px;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{
"type": "property_value",
"value": { "property": "padding", "expected": "1rem" },
"message": "اضبط <kbd>padding: 1rem</kbd>"
},
{ {
"type": "regex", "type": "regex",
"value": "border-bottom:\\s*4px\\s+solid\\s+dodgerblue", "value": "border-left:\\s*4px\\s+solid\\s+coral",
"message": "Set <kbd>border-bottom: 4px solid dodgerblue</kbd>", "message": "اضبط <kbd>border-left: 4px solid coral</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "border-radius", "expected": "4px" },
"message": "اضبط <kbd>border-radius: 4px</kbd>"
} }
] ]
} }

View File

@@ -1,115 +1,100 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "units-variables", "id": "units-variables",
"title": "CSS Units & Variables", "title": "وحدات CSS والمتغيرات",
"description": "Understand the variety of CSS measurement units and how to define and use custom properties for maintainable styles.", "description": "افهم تنوع وحدات القياس في CSS وكيفية تعريف واستخدام الخصائص المخصصة لأنماط قابلة للصيانة.",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "units-1", "id": "units-1",
"title": "Absolute vs. Relative Units", "title": "Relative Units",
"description": "Learn the difference between px, rem, em, %, and vw/vh for flexible, responsive layouts.", "description": "يقدم CSS نوعين من الوحدات: <em>مطلقة</em> (مثل <kbd>px</kbd>) و<em>نسبية</em> (مثل <kbd>%</kbd> و <kbd>rem</kbd>). الوحدات النسبية تتكيف مع سياقها، مما يجعل التخطيطات مرنة وسهلة الوصول.<br><br><strong>الوحدات النسبية الشائعة:</strong><br>• <kbd>%</kbd> نسبة للعنصر الأب<br>• <kbd>rem</kbd> نسبة لحجم خط الجذر (عادة 16px)<br>• <kbd>em</kbd> نسبة لحجم خط العنصر<br><br>نمط شائع للمحتوى القابل للقراءة: اضبط <kbd>width: 100%</kbd> لملء المساحة المتاحة، ثم <kbd>max-width: 40rem</kbd> لتحديد طول السطر للقراءة.",
"task": "Set the <kbd>width</kbd> of <kbd>.box</kbd> to <kbd>80%</kbd> and <kbd>max-width</kbd> to <kbd>37.5rem</kbd>.", "task": "نص هذه المقالة عريض جداً على الشاشات الكبيرة. أضف <kbd>max-width: 40rem</kbd> للحصول على عرض قراءة مثالي.",
"previewHTML": "<div class=\"box\">Resize me!</div>", "previewHTML": "<article class=\"article\"><h2>The Art of Typography</h2><p>Good typography is invisible. When text is set well, readers absorb information without noticing the design decisions that make it comfortable to read. Line length is crucial—too wide and eyes get lost, too narrow and reading becomes choppy.</p><p>The ideal line length is 45-75 characters per line. At typical font sizes, this works out to roughly 40rem maximum width.</p></article>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { background: #f5f5f5; padding: 1rem; }", "previewBaseCSS": "body { font-family: Georgia, serif; padding: 1rem; background: #f9f9f9; } .article { background: white; padding: 2rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .article h2 { margin: 0 0 1rem; color: #333; } .article p { margin: 0 0 1rem; line-height: 1.6; color: #444; } .article p:last-child { margin-bottom: 0; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Set flexible sizing */\n.box {", "codePrefix": ".article {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": " width: 80%;\n max-width: 37.5rem;", "solution": "max-width: 40rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ "type": "contains", "value": "width", "message": "Use <kbd>width</kbd> property", "options": { "caseSensitive": false } },
{ "type": "property_value", "value": { "property": "width", "expected": "80%" }, "message": "Set width to <kbd>80%</kbd>" },
{ "type": "contains", "value": "max-width", "message": "Use <kbd>max-width</kbd> property", "options": { "caseSensitive": false } },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "max-width", "expected": "37.5rem" }, "value": { "property": "max-width", "expected": "40rem" },
"message": "Set max-width to <kbd>37.5rem</kbd>" "message": "اضبط <kbd>max-width: 40rem</kbd>"
} }
] ]
}, },
{ {
"id": "units-2", "id": "units-2",
"title": "CSS Custom Properties", "title": "CSS Variables",
"description": "Define and reuse variables (--custom properties) to centralize your theme values.", "description": "الخصائص المخصصة في CSS (المتغيرات) تتيح لك تعريف قيم قابلة لإعادة الاستخدام. عرّفها بـ <kbd>--اسم</kbd> واستخدمها بـ <kbd>var(--اسم)</kbd>. المتغيرات المعرّفة على <kbd>:root</kbd> متاحة في كل مكان.",
"task": "Create a <kbd>--main-color</kbd> variable in <kbd>:root</kbd> with <kbd>#6200ee</kbd> and apply it as the border color on <kbd>.themed</kbd>.", "task": "عرّف <kbd>--brand: steelblue</kbd> في <kbd>:root</kbd>، ثم استخدمها كلون <kbd>background</kbd> لـ <kbd>.btn</kbd>.",
"previewHTML": "<div class=\"themed\">Variable Box</div>", "previewHTML": "<div class=\"actions\"><button class=\"btn\">Subscribe</button><button class=\"btn\">Learn More</button></div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .themed { padding: 1rem; border: 0.125rem solid #ddd; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .actions { display: flex; gap: 1rem; } .btn { color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 1rem; cursor: pointer; background: #ccc; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Define and use a CSS variable */\n:root {", "codePrefix": ":root {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}\n.themed { }", "codeSuffix": "\n}\n\n.btn {\n background: var(--brand);\n}",
"solution": " --main-color: #6200ee;\n}\n.themed {\n border-color: var(--main-color);", "solution": "--brand: steelblue;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "contains", "type": "contains",
"value": "--main-color", "value": "--brand",
"message": "Define <kbd>--main-color</kbd> in :root", "message": "عرّف المتغير <kbd>--brand</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "contains", "type": "contains",
"value": "var(--main-color)", "value": "steelblue",
"message": "Use <kbd>var(--main-color)</kbd>", "message": "اضبط القيمة على <kbd>steelblue</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "border", "expected": "var(--main-color)" },
"message": "Apply variable to border color",
"options": { "exact": false }
} }
] ]
}, },
{ {
"id": "units-3", "id": "units-3",
"title": "Unit Calculations (calc)", "title": "calc() Function",
"description": "Use the <kbd>calc()</kbd> function to combine different units in one expression.", "description": "دالة <kbd>calc()</kbd> تتيح لك خلط وحدات مختلفة في الحسابات. هذا ضروري للتخطيطات التي تجمع بين الأحجام الثابتة والمرنة، مثل تخطيط الشريط الجانبي.",
"task": "Set the <kbd>width</kbd> of <kbd>.sized</kbd> to <kbd>calc(100% - 2rem)</kbd> and <kbd>min-height</kbd> to <kbd>calc(10vh + 1rem)</kbd>.", "task": "المحتوى الرئيسي يجب أن يملأ المساحة المتبقية بعد الشريط الجانبي 200px. اضبط <kbd>width: calc(100% - 200px)</kbd> على <kbd>.main</kbd>.",
"previewHTML": "<div class=\"sized\">Calc Demo</div>", "previewHTML": "<div class=\"layout\"><aside class=\"sidebar\">Sidebar<br>Navigation</aside><main class=\"main\"><h2>Main Content</h2><p>This area should fill the remaining width after accounting for the fixed-width sidebar.</p></main></div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sized { background: #e8f5e9; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .layout { display: flex; gap: 1rem; } .sidebar { width: 200px; background: #1a1a2e; color: white; padding: 1rem; border-radius: 8px; flex-shrink: 0; } .main { background: white; padding: 1rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .main h2 { margin: 0 0 8px; } .main p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Use calc for dynamic sizing */\n.sized {", "codePrefix": ".main {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": " width: calc(100% - 2rem);\n min-height: calc(10vh + 1rem);", "solution": "width: calc(100% - 200px);",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ "type": "contains", "value": "calc", "message": "Use <kbd>calc()</kbd> function", "options": { "caseSensitive": false } },
{ {
"type": "regex", "type": "regex",
"value": "width:\\s*calc\\(100% - 2rem\\)", "value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
"message": "Width should be <kbd>calc(100% - 2rem)</kbd>", "message": "اضبط <kbd>width: calc(100% - 200px)</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "regex",
"value": "min-height:\\s*calc\\(10vh \\+ 1rem\\)",
"message": "Min-height should be <kbd>calc(10vh + 1rem)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "units-4", "id": "units-4",
"title": "Viewport & Responsive Units", "title": "Viewport Units",
"description": "Control layouts relative to viewport size with vw, vh, and vmin/vmax units.", "description": "وحدات العرض تحدد حجم العناصر نسبة لنافذة المتصفح:<br>• <kbd>vw</kbd> 1% من عرض العرض<br>• <kbd>vh</kbd> 1% من ارتفاع العرض<br><br>هذه مثالية للأقسام بملء الشاشة مثل لافتات hero.",
"task": "Give <kbd>.view</kbd> a <kbd>width</kbd> of <kbd>50vw</kbd> and <kbd>height</kbd> of <kbd>20vh</kbd>.", "task": "اجعل قسم hero هذا يملأ ارتفاع العرض بضبط <kbd>min-height: 100vh</kbd>.",
"previewHTML": "<div class=\"view\">Viewport Box</div>", "previewHTML": "<section class=\"hero\"><h1>Welcome</h1><p>Scroll down to explore</p></section>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .view { background: #ffe0b2; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .hero { background: linear-gradient(135deg, #1a1a2e 0%, steelblue 100%); color: white; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 2rem; } .hero h1 { margin: 0 0 1rem; font-size: 2.5rem; } .hero p { margin: 0; opacity: 0.8; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Use viewport units */\n.view {", "codePrefix": ".hero {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": " width: 50vw;\n height: 20vh;", "solution": "min-height: 100vh;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ "type": "contains", "value": "vw", "message": "Use <kbd>vw</kbd> unit", "options": { "caseSensitive": false } }, {
{ "type": "contains", "value": "vh", "message": "Use <kbd>vh</kbd> unit", "options": { "caseSensitive": false } }, "type": "property_value",
{ "type": "property_value", "value": { "property": "width", "expected": "50vw" }, "message": "Set width to <kbd>50vw</kbd>" }, "value": { "property": "min-height", "expected": "100vh" },
{ "type": "property_value", "value": { "property": "height", "expected": "20vh" }, "message": "Set height to <kbd>20vh</kbd>" } "message": "اضبط <kbd>min-height: 100vh</kbd>"
}
] ]
} }
] ]

View File

@@ -1,15 +1,15 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "transitions-animations", "id": "transitions-animations",
"title": "CSS Animations", "title": "حركات CSS",
"description": "Bring interactivity to your UI by smoothly transitioning properties and creating keyframe-driven animations.", "description": "أضف التفاعل لواجهتك من خلال انتقالات الخصائص السلسة والحركات المبنية على keyframes.",
"difficulty": "intermediate", "difficulty": "intermediate",
"lessons": [ "lessons": [
{ {
"id": "transitions-1", "id": "transitions-1",
"title": "Transitions", "title": "Transitions",
"description": "Learn how to apply <kbd>transition</kbd> to properties for smooth changes on state changes.<br><br><pre>transition: property duration;\n/* e.g. transition: background-color 0.3s; */</pre>", "description": "تعلم كيفية تطبيق <kbd>transition</kbd> على الخصائص للتغييرات السلسة عند تغيير الحالة.<br><br><pre>transition: property duration;\n/* مثال: transition: background-color 0.3s; */</pre>",
"task": "Add <kbd>transition: background-color 0.3s</kbd> to <kbd>.btn</kbd> so the color fades smoothly on hover.", "task": "أضف <kbd>transition: background-color 0.3s</kbd> ليتغير اللون بسلاسة عند التمرير.",
"previewHTML": "<button class=\"btn\">Hover Me</button>", "previewHTML": "<button class=\"btn\">Hover Me</button>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: black; color: white; padding: 0.5rem 1rem; border: none; cursor: pointer; } .btn:hover { background: white; color: black; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: black; color: white; padding: 0.5rem 1rem; border: none; cursor: pointer; } .btn:hover { background: white; color: black; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -22,13 +22,13 @@
{ {
"type": "contains", "type": "contains",
"value": "transition", "value": "transition",
"message": "Use the <kbd>transition</kbd> property", "message": "استخدم خاصية <kbd>transition</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "regex", "type": "regex",
"value": "transition:\\s*background-color\\s*0\\.3s", "value": "transition:\\s*background-color\\s*0\\.3s",
"message": "Set <kbd>transition: background-color 0.3s</kbd>", "message": "اضبط <kbd>transition: background-color 0.3s</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
@@ -36,8 +36,8 @@
{ {
"id": "transitions-2", "id": "transitions-2",
"title": "Timing Funcs", "title": "Timing Funcs",
"description": "Explore easing functions like <kbd>ease</kbd>, <kbd>linear</kbd>, <kbd>ease-in</kbd>, <kbd>ease-out</kbd> to control animation pacing.", "description": "استكشف دوال التسهيل مثل <kbd>ease</kbd>، <kbd>linear</kbd>، <kbd>ease-in</kbd>، <kbd>ease-out</kbd> للتحكم في إيقاع الحركة.",
"task": "Set <kbd>transition-timing-function</kbd> to <kbd>ease-in-out</kbd> on <kbd>.btn</kbd>.", "task": "اضبط <kbd>transition-timing-function</kbd> على <kbd>ease-in-out</kbd>.",
"previewHTML": "<button class=\"btn\">Timing</button>", "previewHTML": "<button class=\"btn\">Timing</button>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: navy; color: gold; padding: 0.5rem 1rem; border: none; cursor: pointer; transition: all 0.5s; } .btn:hover { background: gold; color: navy; transform: scale(1.1); }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: navy; color: gold; padding: 0.5rem 1rem; border: none; cursor: pointer; transition: all 0.5s; } .btn:hover { background: gold; color: navy; transform: scale(1.1); }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -50,21 +50,21 @@
{ {
"type": "contains", "type": "contains",
"value": "transition-timing-function", "value": "transition-timing-function",
"message": "Use <kbd>transition-timing-function</kbd>", "message": "استخدم <kbd>transition-timing-function</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "transition-timing-function", "expected": "ease-in-out" }, "value": { "property": "transition-timing-function", "expected": "ease-in-out" },
"message": "Set timing to <kbd>ease-in-out</kbd>" "message": "اضبط التوقيت على <kbd>ease-in-out</kbd>"
} }
] ]
}, },
{ {
"id": "transitions-3", "id": "transitions-3",
"title": "Keyframes", "title": "Keyframes",
"description": "Create named animations using <kbd>@keyframes</kbd> and apply them via the <kbd>animation</kbd> shorthand.<br><br><pre>@keyframes bounce {\n 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;\n}</pre>", "description": "أنشئ حركات مسماة باستخدام <kbd>@keyframes</kbd> وطبّقها عبر اختصار <kbd>animation</kbd>.<br><br><pre>@keyframes bounce {\n 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;\n}</pre>",
"task": "Define a keyframe at <kbd>50%</kbd> with <kbd>transform: translateY(-20px)</kbd> and apply <kbd>animation: bounce 1s infinite</kbd> to <kbd>.ball</kbd>.", "task": "عرّف keyframe عند <kbd>50%</kbd> مع <kbd>transform: translateY(-20px)</kbd> وطبّق <kbd>animation: bounce 1s infinite</kbd> على <kbd>.ball</kbd>.",
"previewHTML": "<div class=\"ball\"></div>", "previewHTML": "<div class=\"ball\"></div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .ball { width: 50px; height: 50px; background: crimson; border-radius: 50%; margin: 2rem auto; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .ball { width: 50px; height: 50px; background: crimson; border-radius: 50%; margin: 2rem auto; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -77,25 +77,25 @@
{ {
"type": "contains", "type": "contains",
"value": "@keyframes bounce", "value": "@keyframes bounce",
"message": "Define <kbd>@keyframes bounce</kbd>", "message": "عرّف <kbd>@keyframes bounce</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "regex", "type": "regex",
"value": "50%.*transform: translateY\\(-20px\\)", "value": "50%.*transform: translateY\\(-20px\\)",
"message": "At <kbd>50%</kbd>, use <kbd>transform: translateY(-20px)</kbd>", "message": "عند <kbd>50%</kbd>، استخدم <kbd>transform: translateY(-20px)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "contains", "type": "contains",
"value": "animation", "value": "animation",
"message": "Use <kbd>animation</kbd> property on <kbd>.ball</kbd>", "message": "استخدم خاصية <kbd>animation</kbd> على <kbd>.ball</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "regex", "type": "regex",
"value": "animation:.*bounce.*1s.*infinite", "value": "animation:.*bounce.*1s.*infinite",
"message": "Apply <kbd>animation: bounce 1s infinite</kbd>", "message": "طبّق <kbd>animation: bounce 1s infinite</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
@@ -103,8 +103,8 @@
{ {
"id": "transitions-4", "id": "transitions-4",
"title": "Animation Properties", "title": "Animation Properties",
"description": "Fine-tune animations with <kbd>animation-delay</kbd>, <kbd>animation-iteration-count</kbd>, <kbd>animation-direction</kbd>, and <kbd>animation-fill-mode</kbd>.", "description": "اضبط الحركات بـ <kbd>animation-delay</kbd>، <kbd>animation-iteration-count</kbd>، <kbd>animation-direction</kbd>، و <kbd>animation-fill-mode</kbd>.",
"task": "Apply the <kbd>pulse</kbd> animation to <kbd>.box</kbd> with <kbd>animation-name: pulse</kbd>, <kbd>animation-duration: 2s</kbd>, <kbd>animation-delay: 1s</kbd>, <kbd>animation-iteration-count: 2</kbd>, and <kbd>animation-fill-mode: forwards</kbd>.", "task": "طبّق حركة <kbd>pulse</kbd> على <kbd>.box</kbd> مع <kbd>animation-name: pulse</kbd>، <kbd>animation-duration: 2s</kbd>، <kbd>animation-delay: 1s</kbd>، <kbd>animation-iteration-count: 2</kbd>، و <kbd>animation-fill-mode: forwards</kbd>.",
"previewHTML": "<div class=\"box\">Pulse</div>", "previewHTML": "<div class=\"box\">Pulse</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { width: 100px; height: 100px; background: black; color: white; display: flex; align-items: center; justify-content: center; margin: 2rem auto; } @keyframes pulse { 0% { background: black; color: white; transform: scale(1); } 50% { background: white; color: black; transform: scale(1.2); } 100% { background: limegreen; color: black; transform: scale(1); } }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { width: 100px; height: 100px; background: black; color: white; display: flex; align-items: center; justify-content: center; margin: 2rem auto; } @keyframes pulse { 0% { background: black; color: white; transform: scale(1); } 50% { background: white; color: black; transform: scale(1.2); } 100% { background: limegreen; color: black; transform: scale(1); } }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -117,27 +117,27 @@
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-name", "expected": "pulse" }, "value": { "property": "animation-name", "expected": "pulse" },
"message": "Set <kbd>animation-name: pulse</kbd>" "message": "اضبط <kbd>animation-name: pulse</kbd>"
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-duration", "expected": "2s" }, "value": { "property": "animation-duration", "expected": "2s" },
"message": "Set <kbd>animation-duration: 2s</kbd>" "message": "اضبط <kbd>animation-duration: 2s</kbd>"
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-delay", "expected": "1s" }, "value": { "property": "animation-delay", "expected": "1s" },
"message": "Set <kbd>animation-delay: 1s</kbd>" "message": "اضبط <kbd>animation-delay: 1s</kbd>"
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-iteration-count", "expected": "2" }, "value": { "property": "animation-iteration-count", "expected": "2" },
"message": "Set <kbd>animation-iteration-count: 2</kbd>" "message": "اضبط <kbd>animation-iteration-count: 2</kbd>"
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-fill-mode", "expected": "forwards" }, "value": { "property": "animation-fill-mode", "expected": "forwards" },
"message": "Set <kbd>animation-fill-mode: forwards</kbd>" "message": "اضبط <kbd>animation-fill-mode: forwards</kbd>"
} }
] ]
} }

View File

@@ -2,14 +2,14 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "responsive-design", "id": "responsive-design",
"title": "CSS Responsive Design", "title": "CSS Responsive Design",
"description": "Make your layouts adapt to different screen sizes using media queries and fluid design techniques.", "description": "اجعل تخطيطاتك تتكيف مع أحجام الشاشات المختلفة باستخدام media queries وتقنيات التصميم المرن.",
"difficulty": "intermediate", "difficulty": "intermediate",
"lessons": [ "lessons": [
{ {
"id": "responsive-1", "id": "responsive-1",
"title": "Media Queries", "title": "Media Queries",
"description": "Understand the syntax and use cases for CSS media queries to apply styles conditionally based on viewport characteristics.", "description": "افهم صياغة واستخدامات CSS media queries لتطبيق الأنماط شرطياً بناءً على خصائص viewport.<br><br><pre>@media (max-width: 600px) {\n .panel {\n background: lightcoral;\n }\n}</pre>",
"task": "Write a media query with <kbd>@media (max-width: 600px)</kbd> that changes <kbd>.panel</kbd> background to <kbd>lightcoral</kbd>.", "task": "اكتب media query باستخدام <kbd>@media (max-width: 600px)</kbd> لتغيير خلفية <kbd>.panel</kbd> إلى <kbd>lightcoral</kbd>.",
"previewHTML": "<div class=\"panel\">Resize the window</div>", "previewHTML": "<div class=\"panel\">Resize the window</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .panel { padding: 1rem; background: lightblue; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .panel { padding: 1rem; background: lightblue; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -22,19 +22,19 @@
{ {
"type": "regex", "type": "regex",
"value": "@media\\s*\\(max-width:\\s*600px\\)", "value": "@media\\s*\\(max-width:\\s*600px\\)",
"message": "Use <kbd>@media (max-width: 600px)</kbd>", "message": "استخدم <kbd>@media (max-width: 600px)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "contains", "type": "contains",
"value": ".panel", "value": ".panel",
"message": "Target <kbd>.panel</kbd> inside the media query", "message": "استهدف <kbd>.panel</kbd> داخل media query",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "background", "expected": "lightcoral" }, "value": { "property": "background", "expected": "lightcoral" },
"message": "Set <kbd>background: lightcoral</kbd>", "message": "اضبط <kbd>background: lightcoral</kbd>",
"options": { "exact": false } "options": { "exact": false }
} }
] ]
@@ -42,8 +42,8 @@
{ {
"id": "responsive-2", "id": "responsive-2",
"title": "Fluid Type", "title": "Fluid Type",
"description": "Use relative units like <kbd>vw</kbd> to make font sizes scale with the viewport width.", "description": "استخدم وحدات نسبية مثل <kbd>vw</kbd> لجعل أحجام الخطوط تتناسب مع عرض viewport.",
"task": "Set <kbd>font-size: 5vw</kbd> on <kbd>.text</kbd> so it scales as the viewport changes.", "task": "اضبط <kbd>font-size: 5vw</kbd> لتتغير مع viewport.",
"previewHTML": "<p class=\"text\">Fluid Typography</p>", "previewHTML": "<p class=\"text\">Fluid Typography</p>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -53,46 +53,46 @@
"solution": " font-size: 5vw;", "solution": " font-size: 5vw;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "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": "اضبط <kbd>font-size: 5vw</kbd>" }
] ]
}, },
{ {
"id": "responsive-3", "id": "responsive-3",
"title": "Flex Grids", "title": "Responsive Grid",
"description": "Combine CSS Grid with <kbd>auto-fit</kbd> or <kbd>auto-fill</kbd> for responsive column layouts.", "description": "ادمج CSS Grid مع <kbd>auto-fit</kbd> أو <kbd>auto-fill</kbd> لتخطيطات أعمدة متجاوبة تضبط عدد الأعمدة تلقائياً بناءً على المساحة المتاحة.",
"task": "Add <kbd>display: grid</kbd>, <kbd>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</kbd>, and <kbd>gap: 1rem</kbd> to <kbd>.cards</kbd>.", "task": "أضف <kbd>display: grid</kbd> و <kbd>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</kbd> و <kbd>gap: 1rem</kbd>.",
"previewHTML": "<div class=\"cards\"><div>1</div><div>2</div><div>3</div><div>4</div></div>", "previewHTML": "<section class=\"features\"><article class=\"feature\"><h3>Fast</h3><p>Lightning quick load times</p></article><article class=\"feature\"><h3>Secure</h3><p>Enterprise-grade security</p></article><article class=\"feature\"><h3>Reliable</h3><p>99.9% uptime guaranteed</p></article><article class=\"feature\"><h3>Support</h3><p>24/7 customer service</p></article></section>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .cards > div { background: #d1c4e9; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .feature { background: white; padding: 1rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .feature h3 { margin: 0 0 8px; color: steelblue; } .feature p { margin: 0; color: #666; font-size: 0.9rem; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Create a responsive grid */\n.cards {", "codePrefix": ".features {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": "display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;", "solution": "display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "display", "expected": "grid" }, "value": { "property": "display", "expected": "grid" },
"message": "Set <kbd>display: grid</kbd>" "message": "اضبط <kbd>display: grid</kbd>"
}, },
{ {
"type": "regex", "type": "regex",
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)", "value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
"message": "Use <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>", "message": "استخدم <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "gap", "expected": "1rem" }, "value": { "property": "gap", "expected": "1rem" },
"message": "Set <kbd>gap: 1rem</kbd>" "message": "اضبط <kbd>gap: 1rem</kbd>"
} }
] ]
}, },
{ {
"id": "responsive-4", "id": "responsive-4",
"title": "Mobile-First", "title": "Mobile-First",
"description": "Adopt a mobile-first approach by writing base styles for small screens and enhancing for larger viewports.", "description": "اتبع نهج mobile-first بكتابة أنماط أساسية للشاشات الصغيرة وتحسينها لـ viewports أكبر.",
"task": "Write a media query with <kbd>@media (min-width: 768px)</kbd> that sets <kbd>.sidebar</kbd> width to <kbd>250px</kbd>.", "task": "اكتب media query باستخدام <kbd>@media (min-width: 768px)</kbd> لضبط عرض <kbd>.sidebar</kbd> إلى <kbd>250px</kbd>.",
"previewHTML": "<aside class=\"sidebar\">Sidebar</aside>", "previewHTML": "<aside class=\"sidebar\">Sidebar</aside>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -105,19 +105,19 @@
{ {
"type": "regex", "type": "regex",
"value": "@media\\s*\\(min-width:\\s*768px\\)", "value": "@media\\s*\\(min-width:\\s*768px\\)",
"message": "Use <kbd>@media (min-width: 768px)</kbd>", "message": "استخدم <kbd>@media (min-width: 768px)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "contains", "type": "contains",
"value": ".sidebar", "value": ".sidebar",
"message": "Target <kbd>.sidebar</kbd> inside media query", "message": "استهدف <kbd>.sidebar</kbd> في media query",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "width", "expected": "250px" }, "value": { "property": "width", "expected": "250px" },
"message": "Set <kbd>width: 250px</kbd>", "message": "اضبط <kbd>width: 250px</kbd>",
"options": { "exact": false } "options": { "exact": false }
} }
] ]

View File

@@ -2,65 +2,65 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-elements", "id": "html-elements",
"title": "HTML Block & Inline", "title": "HTML Block & Inline",
"description": "Understanding the fundamental difference between container (block) and inline elements", "description": "فهم الفرق الأساسي بين عناصر الحاويات (الكتلية) والعناصر السطرية",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "block-vs-inline-intro", "id": "block-vs-inline-intro",
"title": "Block vs Inline Elements", "title": "العناصر الكتلية vs السطرية",
"description": "HTML elements fall into two main categories:<br><br><strong>Block elements</strong> (containers) start on a new line and take full width. Examples: <kbd>&lt;div&gt;</kbd>, <kbd>&lt;p&gt;</kbd>, <kbd>&lt;h1&gt;</kbd>, <kbd>&lt;section&gt;</kbd><br><br><strong>Inline elements</strong> flow within text and only take needed width. Examples: <kbd>&lt;span&gt;</kbd>, <kbd>&lt;a&gt;</kbd>, <kbd>&lt;strong&gt;</kbd>, <kbd>&lt;em&gt;</kbd>", "description": "تنقسم عناصر HTML إلى فئتين رئيسيتين:<br><br><strong>العناصر الكتلية</strong> (الحاويات) تبدأ في سطر جديد وتأخذ العرض الكامل. أمثلة: <kbd>&lt;div&gt;</kbd>, <kbd>&lt;p&gt;</kbd>, <kbd>&lt;h1&gt;</kbd>, <kbd>&lt;section&gt;</kbd><br><br><strong>العناصر السطرية</strong> تتدفق داخل النص وتأخذ العرض المطلوب فقط. أمثلة: <kbd>&lt;span&gt;</kbd>, <kbd>&lt;a&gt;</kbd>, <kbd>&lt;strong&gt;</kbd>, <kbd>&lt;em&gt;</kbd>",
"task": "Wrap the word <kbd>important</kbd> with <kbd>&lt;strong&gt;</kbd> tags to make it bold. Notice how the paragraph (block) takes full width while strong (inline) flows with text.", "task": "أحط الكلمة <kbd>مهمة</kbd> بوسوم <kbd>&lt;strong&gt;</kbd> لجعلها عريضة. لاحظ كيف يأخذ الفقرة (كتلي) العرض الكامل بينما strong (سطري) يتدفق مع النص.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; } p { background: #e3f2fd; padding: 10px; } strong { background: #ffecb3; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; } p { background: #e3f2fd; padding: 10px; } strong { background: #ffecb3; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<p>This is a paragraph with an important word.</p>", "initialCode": "<p>هذه فقرة تحتوي على كلمة مهمة.</p>",
"solution": "<p>This is a paragraph with an <strong>important</strong> word.</p>", "solution": "<p>هذه فقرة تحتوي على كلمة <strong>مهمة</strong>.</p>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "p", "value": "p",
"message": "Add a <kbd>&lt;p&gt;</kbd> paragraph element" "message": "أضف عنصر فقرة <kbd>&lt;p&gt;</kbd>"
}, },
{ {
"type": "parent_child", "type": "parent_child",
"value": { "parent": "p", "child": "strong" }, "value": { "parent": "p", "child": "strong" },
"message": "Wrap the word <kbd>important</kbd> with <kbd>&lt;strong&gt;</kbd> tags" "message": "أحط الكلمة <kbd>مهمة</kbd> بوسوم <kbd>&lt;strong&gt;</kbd>"
} }
] ]
}, },
{ {
"id": "semantic-containers", "id": "semantic-containers",
"title": "Semantic Tags", "title": "الوسوم الدلالية",
"description": "Modern HTML uses semantic containers that describe their content:<br><br><kbd>&lt;header&gt;</kbd> - Page or section header<br><kbd>&lt;nav&gt;</kbd> - Navigation links<br><kbd>&lt;main&gt;</kbd> - Main content area<br><kbd>&lt;section&gt;</kbd> - Thematic grouping<br><kbd>&lt;article&gt;</kbd> - Self-contained content<br><kbd>&lt;footer&gt;</kbd> - Page or section footer", "description": "يستخدم HTML الحديث حاويات دلالية تصف محتواها:<br><br><kbd>&lt;header&gt;</kbd> - رأس الصفحة أو القسم<br><kbd>&lt;nav&gt;</kbd> - روابط التنقل<br><kbd>&lt;main&gt;</kbd> - منطقة المحتوى الرئيسي<br><kbd>&lt;section&gt;</kbd> - تجميع موضوعي<br><kbd>&lt;article&gt;</kbd> - محتوى مستقل<br><kbd>&lt;footer&gt;</kbd> - تذييل الصفحة أو القسم",
"task": "Create a basic page structure:<br>1. Add a <kbd>&lt;header&gt;</kbd> with an <kbd>&lt;h1&gt;</kbd> containing the text <code>My Website</code><br>2. Add a <kbd>&lt;main&gt;</kbd> element with a paragraph saying <code>Welcome to my site!</code><br>3. Add a <kbd>&lt;footer&gt;</kbd> with a paragraph saying <code>Copyright 2026</code>", "task": "أنشئ هيكل صفحة أساسي:<br>1. أضف <kbd>&lt;header&gt;</kbd> مع <kbd>&lt;h1&gt;</kbd> يحتوي على النص <code>موقعي</code><br>2. أضف عنصر <kbd>&lt;main&gt;</kbd> مع فقرة تقول <code>مرحباً بك في موقعي!</code><br>3. أضف <kbd>&lt;footer&gt;</kbd> مع فقرة تقول <code>Copyright 2026</code>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; margin: 0; } header { background: #1976d2; color: white; padding: 15px; } main { padding: 20px; min-height: 100px; } footer { background: #424242; color: white; padding: 10px; text-align: center; }", "previewBaseCSS": "body { font-family: system-ui; margin: 0; } header { background: #1976d2; color: white; padding: 15px; } main { padding: 20px; min-height: 100px; } footer { background: #424242; color: white; padding: 10px; text-align: center; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<header>\n <h1>My Website</h1>\n</header>\n<main>\n <p>Welcome to my site!</p>\n</main>\n<footer>\n <p>Copyright 2026</p>\n</footer>", "solution": "<header>\n <h1>موقعي</h1>\n</header>\n<main>\n <p>مرحباً بك في موقعي!</p>\n</main>\n<footer>\n <p>Copyright 2026</p>\n</footer>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "header", "value": "header",
"message": "Add a <kbd>&lt;header&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;header&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "main", "value": "main",
"message": "Add a <kbd>&lt;main&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;main&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "footer", "value": "footer",
"message": "Add a <kbd>&lt;footer&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;footer&gt;</kbd>"
}, },
{ {
"type": "parent_child", "type": "parent_child",
"value": { "parent": "header", "child": "h1" }, "value": { "parent": "header", "child": "h1" },
"message": "Add an <kbd>&lt;h1&gt;</kbd> heading inside your header" "message": "أضف عنوان <kbd>&lt;h1&gt;</kbd> داخل header"
} }
] ]
} }

View File

@@ -1,100 +1,100 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-basic", "id": "html-forms-basic",
"title": "HTML Forms", "title": "نماذج HTML",
"description": "Learn to create forms with various input types", "description": "تعلم إنشاء النماذج بأنواع حقول مختلفة",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "form-structure", "id": "form-structure",
"title": "Form Structure", "title": "هيكل النموذج",
"description": "Every form needs a <kbd>&lt;form&gt;</kbd> wrapper. Inside, use <kbd>&lt;label&gt;</kbd> to describe inputs and <kbd>&lt;input&gt;</kbd> for user data entry.<br><br>The <kbd>for</kbd> attribute on labels should match the <kbd>id</kbd> on inputs for accessibility.", "description": "كل نموذج يحتاج غلاف <kbd>&lt;form&gt;</kbd>. بداخله، استخدم <kbd>&lt;label&gt;</kbd> لوصف الحقول و <kbd>&lt;input&gt;</kbd> لإدخال البيانات.<br><br>سمة <kbd>for</kbd> في التسميات يجب أن تطابق <kbd>id</kbd> في الحقول للوصولية.",
"task": "Create a form with:<br>1. A <kbd>&lt;label&gt;</kbd> with the text <code>Name:</code> and <kbd>for=\"name\"</kbd> attribute<br>2. A text <kbd>&lt;input&gt;</kbd> with <kbd>id=\"name\"</kbd> and <kbd>name=\"name\"</kbd> attributes", "task": "أنشئ نموذجاً مع:<br>1. <kbd>&lt;label&gt;</kbd> بالنص <code>الاسم:</code> وسمة <kbd>for=\"name\"</kbd><br>2. <kbd>&lt;input&gt;</kbd> نصي بسمات <kbd>id=\"name\"</kbd> و <kbd>name=\"name\"</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-bottom: 5px; font-weight: 500; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-bottom: 5px; font-weight: 500; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<form>\n <label for=\"name\">Name:</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n</form>", "solution": "<form>\n <label for=\"name\">الاسم:</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n</form>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "form", "value": "form",
"message": "Wrap everything in a <kbd>&lt;form&gt;</kbd> element" "message": "أحط كل شيء بعنصر <kbd>&lt;form&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "label", "value": "label",
"message": "Add a <kbd>&lt;label&gt;</kbd> for your input" "message": "أضف <kbd>&lt;label&gt;</kbd> لحقلك"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "input", "value": "input",
"message": "Add an <kbd>&lt;input&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;input&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "label", "attr": "for", "value": null }, "value": { "selector": "label", "attr": "for", "value": null },
"message": "Add a <kbd>for</kbd> attribute to your label" "message": "أضف سمة <kbd>for</kbd> للتسمية"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input", "attr": "id", "value": null }, "value": { "selector": "input", "attr": "id", "value": null },
"message": "Add an <kbd>id</kbd> attribute to your input" "message": "أضف سمة <kbd>id</kbd> لحقلك"
} }
] ]
}, },
{ {
"id": "input-types", "id": "input-types",
"title": "Input Types", "title": "أنواع الحقول",
"description": "Different input types provide appropriate keyboards and validation:<br><br><kbd>type=\"text\"</kbd> - General text<br><kbd>type=\"email\"</kbd> - Email with @ validation<br><kbd>type=\"password\"</kbd> - Hidden characters<br><kbd>type=\"number\"</kbd> - Numeric keyboard<br><kbd>type=\"tel\"</kbd> - Phone keyboard", "description": "أنواع الحقول المختلفة توفر لوحات مفاتيح وتحقق مناسب:<br><br><kbd>type=\"text\"</kbd> - نص عام<br><kbd>type=\"email\"</kbd> - بريد إلكتروني مع تحقق @<br><kbd>type=\"password\"</kbd> - أحرف مخفية<br><kbd>type=\"number\"</kbd> - لوحة مفاتيح رقمية<br><kbd>type=\"tel\"</kbd> - لوحة مفاتيح هاتف",
"task": "Create a login form with two fields:<br>1. An email field: <kbd>&lt;label for=\"email\"&gt;Email:&lt;/label&gt;</kbd> and <kbd>&lt;input type=\"email\" id=\"email\"&gt;</kbd><br>2. A password field: <kbd>&lt;label for=\"password\"&gt;Password:&lt;/label&gt;</kbd> and <kbd>&lt;input type=\"password\" id=\"password\"&gt;</kbd>", "task": "أنشئ نموذج تسجيل دخول بحقلين:<br>1. حقل بريد: <kbd>&lt;label for=\"email\"&gt;البريد:&lt;/label&gt;</kbd> و <kbd>&lt;input type=\"email\" id=\"email\"&gt;</kbd><br>2. حقل كلمة مرور: <kbd>&lt;label for=\"password\"&gt;كلمة المرور:&lt;/label&gt;</kbd> و <kbd>&lt;input type=\"password\" id=\"password\"&gt;</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<form>\n \n</form>", "initialCode": "<form>\n \n</form>",
"solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\">\n</form>", "solution": "<form>\n <label for=\"email\">البريد:</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <label for=\"password\">كلمة المرور:</label>\n <input type=\"password\" id=\"password\" name=\"password\">\n</form>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "input[type='email']", "value": "input[type='email']",
"message": "Add an input with type=\"email\"" "message": "أضف حقل بـ type=\"email\""
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "input[type='password']", "value": "input[type='password']",
"message": "Add an input with type=\"password\"" "message": "أضف حقل بـ type=\"password\""
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "label", "min": 2 }, "value": { "selector": "label", "min": 2 },
"message": "Add labels for both inputs" "message": "أضف تسميات لكلا الحقلين"
} }
] ]
}, },
{ {
"id": "submit-button", "id": "submit-button",
"title": "Submit Button", "title": "زر الإرسال",
"description": "Forms need a way to submit data. Use:<br><br><kbd>&lt;button type=\"submit\"&gt;</kbd> - Preferred, flexible content<br><kbd>&lt;input type=\"submit\"&gt;</kbd> - Simple text-only button<br><br>The button text should be action-oriented (e.g., <code>Sign In</code>, 'Register', 'Send').", "description": "النماذج تحتاج طريقة لإرسال البيانات. استخدم:<br><br><kbd>&lt;button type=\"submit\"&gt;</kbd> - مفضل، محتوى مرن<br><kbd>&lt;input type=\"submit\"&gt;</kbd> - زر نص بسيط<br><br>نص الزر يجب أن يكون موجه للعمل (مثل <code>تسجيل الدخول</code>، 'التسجيل'، 'إرسال').",
"task": "Add a submit button to the form with the text <code>Sign In</code>.", "task": "أضف زر إرسال للنموذج بالنص <code>تسجيل الدخول</code>.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } button { width: 100%; margin-top: 20px; padding: 10px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } button:hover { background: #1565c0; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } button { width: 100%; margin-top: 20px; padding: 10px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } button:hover { background: #1565c0; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n \n</form>", "initialCode": "<form>\n <label for=\"email\">البريد:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">كلمة المرور:</label>\n <input type=\"password\" id=\"password\">\n \n</form>",
"solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n \n <button type=\"submit\">Sign In</button>\n</form>", "solution": "<form>\n <label for=\"email\">البريد:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">كلمة المرور:</label>\n <input type=\"password\" id=\"password\">\n \n <button type=\"submit\">تسجيل الدخول</button>\n</form>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "button[type='submit'], input[type='submit']", "value": "button[type='submit'], input[type='submit']",
"message": "Add a submit button to your form" "message": "أضف زر إرسال لنموذجك"
}, },
{ {
"type": "element_text", "type": "element_text",
"value": { "selector": "button", "text": "Sign In" }, "value": { "selector": "button", "text": "تسجيل الدخول" },
"message": "The button should say <kbd>Sign In</kbd>" "message": "يجب أن يعرض الزر <kbd>تسجيل الدخول</kbd>"
} }
] ]
} }

View File

@@ -1,110 +1,32 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-validation", "id": "html-forms-validation",
"title": "HTML Validation", "title": "تحقق النماذج",
"description": "Learn HTML5 built-in form validation attributes", "description": "استخدم تحقق HTML5 المدمج لتجربة مستخدم أفضل",
"mode": "html", "mode": "html",
"difficulty": "intermediate", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "required-fields", "id": "required-fields",
"title": "Required Fields", "title": "الحقول المطلوبة",
"description": "The <kbd>required</kbd> attribute prevents form submission if the field is empty.<br><br>Add it to any input that must be filled:<br><kbd>&lt;input type=\"text\" required&gt;</kbd><br><br>The browser shows a validation message automatically.", "description": "سمة <kbd>required</kbd> تمنع إرسال النموذج إذا كان الحقل فارغاً. يعرض المتصفح رسالة تحقق تلقائياً - بدون JavaScript!<br><br>أضفها لأي حقل يجب ملؤه:<br><kbd>&lt;input type=\"text\" required&gt;</kbd>",
"task": "Make both the name and email fields required by adding the <kbd>required</kbd> attribute.", "task": "اجعل كلا الحقلين (الاسم والبريد) مطلوبين بإضافة سمة <kbd>required</kbd> لكل حقل.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-of-type { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid { border-color: #d32f2f; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 320px; } label { display: block; margin-top: 15px; margin-bottom: 5px; font-weight: 500; } label:first-of-type { margin-top: 0; } input { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; } input:focus { outline: 2px solid steelblue; border-color: transparent; } button { margin-top: 20px; padding: 10px 20px; background: steelblue; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 500; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n \n <label for=\"email\">Email: *</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <button type=\"submit\">Submit</button>\n</form>", "initialCode": "<form>\n <label for=\"name\">الاسم *</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n \n <label for=\"email\">البريد *</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <button type=\"submit\">إرسال</button>\n</form>",
"solution": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n \n <label for=\"email\">Email: *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <button type=\"submit\">Submit</button>\n</form>", "solution": "<form>\n <label for=\"name\">الاسم *</label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n \n <label for=\"email\">البريد *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <button type=\"submit\">إرسال</button>\n</form>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input[name='name']", "attr": "required", "value": true }, "value": { "selector": "input[name='name']", "attr": "required", "value": true },
"message": "Add the <kbd>required</kbd> attribute to the name input" "message": "أضف <kbd>required</kbd> لحقل الاسم"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input[name='email']", "attr": "required", "value": true }, "value": { "selector": "input[name='email']", "attr": "required", "value": true },
"message": "Add the <kbd>required</kbd> attribute to the email input" "message": "أضف <kbd>required</kbd> لحقل البريد"
}
]
},
{
"id": "input-constraints",
"title": "Constraints",
"description": "Control what users can enter:<br><br><kbd>minlength</kbd> / <kbd>maxlength</kbd> - Text length limits<br><kbd>min</kbd> / <kbd>max</kbd> - Number range<br><kbd>pattern</kbd> - Regex pattern matching<br><kbd>placeholder</kbd> - Hint text (not a label!)",
"task": "Add validation to the password input:<br>1. Add <kbd>minlength=\"8\"</kbd> for minimum length<br>2. Add <kbd>maxlength=\"20\"</kbd> for maximum length<br>3. Add <kbd>placeholder=\"Enter password\"</kbd> as a hint",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } label { display: block; margin-bottom: 5px; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid:not(:placeholder-shown) { border-color: #d32f2f; } small { display: block; font-size: 12px; color: #666; margin-top: 4px; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }",
"sandboxCSS": "",
"initialCode": "<form>\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Must be 8-20 characters</small>\n \n <button type=\"submit\">Create Account</button>\n</form>",
"solution": "<form>\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\" maxlength=\"20\" placeholder=\"Enter password\" aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Must be 8-20 characters</small>\n \n <button type=\"submit\">Create Account</button>\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "minlength", "value": "8" },
"message": "Add <kbd>minlength</kbd>=\"8\" to the password input"
},
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "maxlength", "value": "20" },
"message": "Add <kbd>maxlength</kbd>=\"20\" to the password input"
},
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "placeholder", "value": null },
"message": "Add a <kbd>placeholder</kbd> to hint what to enter"
}
]
},
{
"id": "complete-registration",
"title": "Full Form",
"description": "Build a complete registration form with all validation concepts:<br><br>- Required fields marked with *<br>- Email validation (use type=\"email\")<br>- Password with length constraints<br>- Terms checkbox (required)<br>- Submit button",
"task": "Complete the registration form. Add required attributes, proper input types, and validation constraints.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 400px; background: #f5f5f5; padding: 25px; border-radius: 8px; } h2 { margin-top: 0; margin-bottom: 20px; } label { display: block; margin-top: 15px; margin-bottom: 5px; font-weight: 500; } input[type='text'], input[type='email'], input[type='password'] { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 14px; } input:focus { outline: 2px solid #1976d2; border-color: transparent; } input[type='checkbox'] { width: auto; margin-right: 8px; vertical-align: middle; } label:has(input[type='checkbox']) { display: flex; align-items: center; font-weight: normal; } button { width: 100%; margin-top: 20px; padding: 12px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 500; } button:hover { background: #1565c0; }",
"sandboxCSS": "",
"initialCode": "<form>\n <h2>Create Account</h2>\n \n <label for=\"fullname\">Full Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\">\n \n <label for=\"email\">Email *</label>\n <input id=\"email\" name=\"email\">\n \n <label for=\"password\">Password *</label>\n <input id=\"password\" name=\"password\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\">\n I agree to the Terms of Service *\n </label>\n \n <button type=\"submit\">Register</button>\n</form>",
"solution": "<form>\n <h2>Create Account</h2>\n \n <label for=\"fullname\">Full Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\" required>\n \n <label for=\"email\">Email *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <label for=\"password\">Password *</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\" required>\n I agree to the Terms of Service *\n </label>\n \n <button type=\"submit\">Register</button>\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "attribute_value",
"value": { "selector": "#fullname", "attr": "required", "value": true },
"message": "Make the full name field <kbd>required</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#email", "attr": "type", "value": "email" },
"message": "Set the email input <kbd>type=\"email\"</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#email", "attr": "required", "value": true },
"message": "Make the email field <kbd>required</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "type", "value": "password" },
"message": "Set the password input <kbd>type=\"password\"</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "required", "value": true },
"message": "Make the password field <kbd>required</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "minlength", "value": "8" },
"message": "Add <kbd>minlength</kbd>=\"8\" to password"
},
{
"type": "attribute_value",
"value": { "selector": "#terms", "attr": "required", "value": true },
"message": "Make the terms checkbox <kbd>required</kbd>"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-details-summary", "id": "html-details-summary",
"title": "HTML Details & Summary", "title": "HTML Details & Summary",
"description": "Create expandable content sections without JavaScript", "description": "أنشئ أقسام قابلة للتوسيع بدون JavaScript",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "details-summary-basic", "id": "details-summary-basic",
"title": "First Widget", "title": "أول عنصر تفاعلي",
"description": "The <kbd>&lt;details&gt;</kbd> element creates a collapsible section. The <kbd>&lt;summary&gt;</kbd> provides the clickable label.<br><br>Click the summary to toggle the hidden content - no JavaScript needed!", "description": "عنصر <kbd>&lt;details&gt;</kbd> ينشئ قسماً قابلاً للطي. عنصر <kbd>&lt;summary&gt;</kbd> يوفر التسمية القابلة للنقر.<br><br>انقر على الملخص لإظهار المحتوى المخفي - بدون JavaScript!",
"task": "Create a <kbd>&lt;details&gt;</kbd> element with:<br>1. A <kbd>&lt;summary&gt;</kbd> saying <code>Click to reveal</code><br>2. A <kbd>&lt;p&gt;</kbd> with the text <code>This content was hidden!</code>", "task": "أنشئ عنصر <kbd>&lt;details&gt;</kbd> مع:<br>1. عنصر <kbd>&lt;summary&gt;</kbd> يقول <code>Click to reveal</code><br>2. عنصر <kbd>&lt;p&gt;</kbd> بالنص <code>This content was hidden!</code>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; } summary { font-weight: 600; cursor: pointer; } summary:hover { color: #1976d2; } details p { margin-top: 15px; color: #666; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; } summary { font-weight: 600; cursor: pointer; } summary:hover { color: #1976d2; } details p { margin-top: 15px; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,30 +21,30 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "details", "value": "details",
"message": "Add a <kbd>&lt;details&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;details&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "summary", "value": "summary",
"message": "Add a <kbd>&lt;summary&gt;</kbd> inside the details" "message": "أضف <kbd>&lt;summary&gt;</kbd> داخل details"
}, },
{ {
"type": "parent_child", "type": "parent_child",
"value": { "parent": "details", "child": "summary" }, "value": { "parent": "details", "child": "summary" },
"message": "The <kbd>&lt;summary&gt;</kbd> must be inside <kbd>&lt;details&gt;</kbd>" "message": "يجب أن يكون <kbd>&lt;summary&gt;</kbd> داخل <kbd>&lt;details&gt;</kbd>"
}, },
{ {
"type": "parent_child", "type": "parent_child",
"value": { "parent": "details", "child": "p" }, "value": { "parent": "details", "child": "p" },
"message": "Add a <kbd>&lt;p&gt;</kbd> inside <kbd>&lt;details&gt;</kbd> for the hidden content" "message": "أضف <kbd>&lt;p&gt;</kbd> داخل <kbd>&lt;details&gt;</kbd> للمحتوى المخفي"
} }
] ]
}, },
{ {
"id": "details-open-attribute", "id": "details-open-attribute",
"title": "Pre-expanded Details", "title": "موسع افتراضياً",
"description": "By default, <kbd>&lt;details&gt;</kbd> is closed. Add the <kbd>open</kbd> attribute to show the content initially.<br><br>This is a boolean attribute - just add <kbd>open</kbd> without a value.", "description": "افتراضياً، <kbd>&lt;details&gt;</kbd> مغلق. أضف سمة <kbd>open</kbd> لإظهار المحتوى في البداية.<br><br>هذه سمة منطقية - فقط أضف <kbd>open</kbd> بدون قيمة.",
"task": "Add the <kbd>open</kbd> attribute to the <kbd>&lt;details&gt;</kbd> element to show the content by default.", "task": "أضف سمة <kbd>open</kbd> لعنصر <kbd>&lt;details&gt;</kbd> لإظهار المحتوى افتراضياً.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; background: #f9f9f9; } summary { font-weight: 600; cursor: pointer; } details p { margin-top: 15px; color: #666; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; background: #f9f9f9; } summary { font-weight: 600; cursor: pointer; } details p { margin-top: 15px; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -55,15 +55,15 @@
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "details", "attr": "open", "value": true }, "value": { "selector": "details", "attr": "open", "value": true },
"message": "Add the <kbd>open</kbd> attribute to <kbd>&lt;details&gt;</kbd>" "message": "أضف سمة <kbd>open</kbd> إلى <kbd>&lt;details&gt;</kbd>"
} }
] ]
}, },
{ {
"id": "faq-accordion", "id": "faq-accordion",
"title": "FAQ Accordion", "title": "أكورديون FAQ",
"description": "Multiple <kbd>&lt;details&gt;</kbd> elements create an accordion-style FAQ. Each question can be expanded independently.<br><br><b>Pro tip:</b> Type <kbd>details*3&gt;summary+p</kbd> and press Tab for Emmet expansion. <kbd>*3</kbd> creates 3 elements, <kbd>&gt;</kbd> nests inside, <kbd>+</kbd> adds siblings.", "description": "عناصر <kbd>&lt;details&gt;</kbd> المتعددة تنشئ FAQ بأسلوب الأكورديون. كل سؤال يمكن توسيعه بشكل مستقل.<br><br><b>نصيحة:</b> اكتب <kbd>details*3&gt;summary+p</kbd> واضغط Tab لتوسيع Emmet. <kbd>*3</kbd> ينشئ 3 عناصر، <kbd>&gt;</kbd> يضع بالداخل، <kbd>+</kbd> يضيف أشقاء.",
"task": "Create an FAQ section with:<br>1. An <kbd>&lt;h1&gt;</kbd> saying <code>Frequently Asked Questions</code><br>2. Three <kbd>&lt;details&gt;</kbd> elements, each with a question in <kbd>&lt;summary&gt;</kbd> and an answer in <kbd>&lt;p&gt;</kbd>", "task": "أنشئ قسم FAQ مع:<br>1. عنصر <kbd>&lt;h1&gt;</kbd> يقول <code>Frequently Asked Questions</code><br>2. ثلاثة عناصر <kbd>&lt;details&gt;</kbd>، كل واحد بسؤال في <kbd>&lt;summary&gt;</kbd> وإجابة في <kbd>&lt;p&gt;</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; min-height: 100vh; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); display: flex; flex-direction: column; justify-content: center; padding: 40px; margin: 0; box-sizing: border-box; } h1 { font-size: 2.5rem; color: #4a4a4a; text-align: center; margin: 0 0 30px 0; } details { background: white; border-radius: 12px; margin-bottom: 15px; padding: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } summary { font-size: 1.3rem; font-weight: 600; color: #5c5c5c; cursor: pointer; list-style: none; } summary::before { content: '▸ '; color: #fcb69f; } details[open] summary::before { content: '▾ '; } details p { margin: 15px 0 0 0; color: #666; line-height: 1.6; }", "previewBaseCSS": "body { font-family: system-ui; min-height: 100vh; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); display: flex; flex-direction: column; justify-content: center; padding: 40px; margin: 0; box-sizing: border-box; } h1 { font-size: 2.5rem; color: #4a4a4a; text-align: center; margin: 0 0 30px 0; } details { background: white; border-radius: 12px; margin-bottom: 15px; padding: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } summary { font-size: 1.3rem; font-weight: 600; color: #5c5c5c; cursor: pointer; list-style: none; } summary::before { content: '▸ '; color: #fcb69f; } details[open] summary::before { content: '▾ '; } details p { margin: 15px 0 0 0; color: #666; line-height: 1.6; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -74,22 +74,22 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "h1", "value": "h1",
"message": "Add an <kbd>&lt;h1&gt;</kbd> heading for the FAQ title" "message": "أضف عنوان <kbd>&lt;h1&gt;</kbd> لعنوان FAQ"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "details", "min": 3 }, "value": { "selector": "details", "min": 3 },
"message": "Create at least 3 <kbd>&lt;details&gt;</kbd> elements for the FAQ" "message": "أنشئ على الأقل 3 عناصر <kbd>&lt;details&gt;</kbd> للـ FAQ"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "summary", "min": 3 }, "value": { "selector": "summary", "min": 3 },
"message": "Each <kbd>&lt;details&gt;</kbd> needs a <kbd>&lt;summary&gt;</kbd> for the question" "message": "كل <kbd>&lt;details&gt;</kbd> يحتاج <kbd>&lt;summary&gt;</kbd> للسؤال"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "details p", "min": 3 }, "value": { "selector": "details p", "min": 3 },
"message": "Each <kbd>&lt;details&gt;</kbd> needs a <kbd>&lt;p&gt;</kbd> for the answer" "message": "كل <kbd>&lt;details&gt;</kbd> يحتاج <kbd>&lt;p&gt;</kbd> للإجابة"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-progress-meter", "id": "html-progress-meter",
"title": "HTML Progress & Meter", "title": "HTML Progress & Meter",
"description": "Display completion status and scalar measurements natively", "description": "اعرض حالة الإكمال والقياسات بشكل أصلي",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "progress-basic", "id": "progress-basic",
"title": "Progress Bars", "title": "أشرطة التقدم",
"description": "The <kbd>&lt;progress&gt;</kbd> element shows task completion. Use <kbd>value</kbd> for current progress and <kbd>max</kbd> for the total.<br><br><b>Note:</b> This is not a self-closing tag! Write <kbd>&lt;progress&gt;...&lt;/progress&gt;</kbd> with fallback text inside for older browsers.", "description": "عنصر <kbd>&lt;progress&gt;</kbd> يُظهر إكمال المهمة. استخدم <kbd>value</kbd> للتقدم الحالي و <kbd>max</kbd> للإجمالي.<br><br><b>ملاحظة:</b> هذا ليس وسماً ذاتي الإغلاق! اكتب <kbd>&lt;progress&gt;...&lt;/progress&gt;</kbd> مع نص بديل بالداخل للمتصفحات القديمة.",
"task": "Create a progress bar showing 70% completion:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Download:</code><br>2. Add a <kbd>&lt;progress&gt;</kbd> with <kbd>value=\"70\"</kbd> and <kbd>max=\"100\"</kbd>", "task": "أنشئ شريط تقدم يُظهر 70% إكمال:<br>1. أضف <kbd>&lt;label&gt;</kbd> يقول <code>Download:</code><br>2. أضف <kbd>&lt;progress&gt;</kbd> مع <kbd>value=\"70\"</kbd> و <kbd>max=\"100\"</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } progress { width: 100%; height: 20px; border-radius: 10px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 10px; } progress::-webkit-progress-value { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; } progress::-moz-progress-bar { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } progress { width: 100%; height: 20px; border-radius: 10px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 10px; } progress::-webkit-progress-value { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; } progress::-moz-progress-bar { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,30 +21,30 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "progress", "value": "progress",
"message": "Add a <kbd>&lt;progress&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;progress&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "progress", "attr": "value", "value": "70" }, "value": { "selector": "progress", "attr": "value", "value": "70" },
"message": "Set <kbd>value=</kbd>\"70\" on the progress element" "message": "عيّن <kbd>value=</kbd>\"70\" في عنصر progress"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "progress", "attr": "max", "value": "100" }, "value": { "selector": "progress", "attr": "max", "value": "100" },
"message": "Set <kbd>max=</kbd>\"100\" on the progress element" "message": "عيّن <kbd>max=</kbd>\"100\" في عنصر progress"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "label", "value": "label",
"message": "Add a <kbd>&lt;label&gt;</kbd> for the progress bar" "message": "أضف <kbd>&lt;label&gt;</kbd> لشريط التقدم"
} }
] ]
}, },
{ {
"id": "progress-indeterminate", "id": "progress-indeterminate",
"title": "Indeterminate Progress", "title": "تقدم غير محدد",
"description": "When progress is unknown (like loading), omit the <kbd>value</kbd> attribute. This creates an animated indeterminate state.<br><br>Useful for network requests or processes with unknown duration.", "description": "عندما يكون التقدم غير معروف (مثل التحميل)، احذف سمة <kbd>value</kbd>. هذا ينشئ حالة متحركة غير محددة.<br><br>مفيد لطلبات الشبكة أو العمليات ذات المدة غير المعروفة.",
"task": "Create a loading indicator:<br>1. Add a <kbd>&lt;p&gt;</kbd> saying <code>Loading...</code><br>2. Add a <kbd>&lt;progress&gt;</kbd> without a value attribute", "task": "أنشئ مؤشر تحميل:<br>1. أضف <kbd>&lt;p&gt;</kbd> يقول <code>Loading...</code><br>2. أضف <kbd>&lt;progress&gt;</kbd> بدون سمة value",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } p { margin-bottom: 10px; color: #666; } progress { width: 100%; height: 8px; border-radius: 4px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 4px; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } p { margin-bottom: 10px; color: #666; } progress { width: 100%; height: 8px; border-radius: 4px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 4px; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -55,20 +55,20 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "progress", "value": "progress",
"message": "Add a <kbd>&lt;progress&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;progress&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "p", "value": "p",
"message": "Add a <kbd>&lt;p&gt;</kbd> with loading text" "message": "أضف <kbd>&lt;p&gt;</kbd> مع نص التحميل"
} }
] ]
}, },
{ {
"id": "meter-gauge", "id": "meter-gauge",
"title": "Meter Gauges", "title": "مقاييس meter",
"description": "The <kbd>&lt;meter&gt;</kbd> element displays a scalar value within a range. Use it for measurements like disk space, battery, or ratings.<br><br>Set <kbd>low</kbd>, <kbd>high</kbd>, and <kbd>optimum</kbd> to define good/bad ranges - the browser colors it accordingly!", "description": "عنصر <kbd>&lt;meter&gt;</kbd> يعرض قيمة قياسية ضمن نطاق. استخدمه للقياسات مثل مساحة القرص، البطارية، أو التقييمات.<br><br>عيّن <kbd>low</kbd> و <kbd>high</kbd> و <kbd>optimum</kbd> لتحديد النطاقات الجيدة/السيئة - المتصفح يلونها وفقاً لذلك!",
"task": "Create a battery level meter:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Battery:</code><br>2. Add a <kbd>&lt;meter&gt;</kbd> with:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> and <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> and <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>", "task": "أنشئ مقياس مستوى البطارية:<br>1. أضف <kbd>&lt;label&gt;</kbd> يقول <code>Battery:</code><br>2. أضف <kbd>&lt;meter&gt;</kbd> مع:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> و <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> و <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } meter { width: 100%; height: 25px; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } meter { width: 100%; height: 25px; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -79,22 +79,42 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "meter", "value": "meter",
"message": "Add a <kbd>&lt;meter&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;meter&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "meter", "attr": "value", "value": "0.8" }, "value": { "selector": "meter", "attr": "value", "value": "0.8" },
"message": "Set <kbd>value=</kbd>\"0.8\" on the meter" "message": "عيّن <kbd>value=</kbd>\"0.8\" في meter"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "min", "value": "0" },
"message": "عيّن <kbd>min=</kbd>\"0\" في meter"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "max", "value": "1" },
"message": "عيّن <kbd>max=</kbd>\"1\" في meter"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "meter", "attr": "low", "value": "0.2" }, "value": { "selector": "meter", "attr": "low", "value": "0.2" },
"message": "Set <kbd>low=</kbd>\"0.2\" to define the low threshold" "message": "عيّن <kbd>low=</kbd>\"0.2\" لتحديد العتبة المنخفضة"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "high", "value": "0.8" },
"message": "عيّن <kbd>high=</kbd>\"0.8\" لتحديد العتبة العالية"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "optimum", "value": "1" },
"message": "عيّن <kbd>optimum=</kbd>\"1\" للإشارة إلى القيمة المثلى"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "label", "value": "label",
"message": "Add a <kbd>&lt;label&gt;</kbd> for the meter" "message": "أضف <kbd>&lt;label&gt;</kbd> للـ meter"
} }
] ]
} }

View File

@@ -1,16 +1,16 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-datalist", "id": "html-datalist",
"title": "Datalist", "title": "قائمة البيانات",
"description": "Provide suggestions for text inputs without JavaScript", "description": "قدم اقتراحات لحقول النص بدون JavaScript",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "datalist-basic", "id": "datalist-basic",
"title": "Input with Suggestions", "title": "حقل مع اقتراحات",
"description": "The <kbd>&lt;datalist&gt;</kbd> element provides autocomplete suggestions for inputs. Connect it using the <kbd>list</kbd> attribute on the input matching the datalist's <kbd>id</kbd>.<br><br>Users can still type freely - suggestions are just helpers!", "description": "عنصر <kbd>&lt;datalist&gt;</kbd> يوفر اقتراحات الإكمال التلقائي للحقول. اربطه باستخدام سمة <kbd>list</kbd> في الحقل بما يتطابق مع <kbd>id</kbd> قائمة البيانات.<br><br>يمكن للمستخدمين الكتابة بحرية - الاقتراحات مجرد مساعدات!",
"task": "Create a browser selector:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Browser:</code><br>2. Add an <kbd>&lt;input&gt;</kbd> with <kbd>list=\"browsers\"</kbd><br>3. Add a <kbd>&lt;datalist id=\"browsers\"&gt;</kbd> with options for Chrome, Firefox, and Safari", "task": "أنشئ محدد متصفح:<br>1. أضف <kbd>&lt;label&gt;</kbd> يقول <code>المتصفح:</code><br>2. أضف <kbd>&lt;input&gt;</kbd> مع <kbd>list=\"browsers\"</kbd><br>3. أضف <kbd>&lt;datalist id=\"browsers\"&gt;</kbd> مع خيارات Chrome و Firefox و Safari",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } input { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; font-size: 16px; } input:focus { outline: 2px solid #1976d2; border-color: transparent; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } input { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; font-size: 16px; } input:focus { outline: 2px solid #1976d2; border-color: transparent; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,30 +21,30 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "datalist", "value": "datalist",
"message": "Add a <kbd>&lt;datalist&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;datalist&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input", "attr": "list", "value": "browsers" }, "value": { "selector": "input", "attr": "list", "value": "browsers" },
"message": "Connect the input to datalist using <kbd>list=</kbd>\"browsers\"" "message": "اربط الحقل بقائمة البيانات باستخدام <kbd>list=</kbd>\"browsers\""
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "option", "min": 3 }, "value": { "selector": "option", "min": 3 },
"message": "Add at least 3 <kbd>&lt;option&gt;</kbd> elements inside <kbd>&lt;datalist&gt;</kbd>" "message": "أضف على الأقل 3 عناصر <kbd>&lt;option&gt;</kbd> داخل <kbd>&lt;datalist&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "label", "value": "label",
"message": "Add a <kbd>&lt;label&gt;</kbd> for the input" "message": "أضف <kbd>&lt;label&gt;</kbd> للحقل"
} }
] ]
}, },
{ {
"id": "datalist-countries", "id": "datalist-countries",
"title": "Country Selector", "title": "محدد الدول",
"description": "Datalists work great for long lists like countries. Users can type to filter suggestions instantly.<br><br>The <kbd>value</kbd> attribute is what gets entered, and you can add display text after it.", "description": "قوائم البيانات تعمل بشكل رائع للقوائم الطويلة مثل الدول. يمكن للمستخدمين الكتابة لتصفية الاقتراحات فوراً.<br><br>سمة <kbd>value</kbd> هي ما يتم إدخاله، ويمكنك إضافة نص عرض بعدها.",
"task": "Create a country input:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Country:</code><br>2. Add an <kbd>&lt;input&gt;</kbd> with <kbd>list=\"countries\"</kbd><br>3. Add a <kbd>&lt;datalist id=\"countries\"&gt;</kbd> with at least 4 country options", "task": "أنشئ حقل دولة:<br>1. أضف <kbd>&lt;label&gt;</kbd> يقول <code>الدولة:</code><br>2. أضف <kbd>&lt;input&gt;</kbd> مع <kbd>list=\"countries\"</kbd><br>3. أضف <kbd>&lt;datalist id=\"countries\"&gt;</kbd> مع 4 خيارات دول على الأقل",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 30px; background: linear-gradient(135deg, #e0f7fa 0%, #b2ebf2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } label { display: block; margin-bottom: 10px; font-weight: 600; color: #00695c; } input { width: 100%; padding: 12px 15px; border: 2px solid #26a69a; border-radius: 8px; font-size: 16px; background: white; } input:focus { outline: none; border-color: #00695c; box-shadow: 0 0 0 3px rgba(38,166,154,0.2); }", "previewBaseCSS": "body { font-family: system-ui; padding: 30px; background: linear-gradient(135deg, #e0f7fa 0%, #b2ebf2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } label { display: block; margin-bottom: 10px; font-weight: 600; color: #00695c; } input { width: 100%; padding: 12px 15px; border: 2px solid #26a69a; border-radius: 8px; font-size: 16px; background: white; } input:focus { outline: none; border-color: #00695c; box-shadow: 0 0 0 3px rgba(38,166,154,0.2); }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -55,22 +55,22 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "datalist", "value": "datalist",
"message": "Add a <kbd>&lt;datalist&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;datalist&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "datalist", "attr": "id", "value": "countries" }, "value": { "selector": "datalist", "attr": "id", "value": "countries" },
"message": "Set <kbd>id=</kbd>\"countries\" on the datalist" "message": "عيّن <kbd>id=</kbd>\"countries\" في قائمة البيانات"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input", "attr": "list", "value": "countries" }, "value": { "selector": "input", "attr": "list", "value": "countries" },
"message": "Connect the input using <kbd>list=</kbd>\"countries\"" "message": "اربط الحقل باستخدام <kbd>list=</kbd>\"countries\""
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "option", "min": 4 }, "value": { "selector": "option", "min": 4 },
"message": "Add at least 4 country options" "message": "أضف على الأقل 4 خيارات دول"
} }
] ]
} }

View File

@@ -1,16 +1,16 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-dialog", "id": "html-dialog",
"title": "Dialogs", "title": "مربعات الحوار",
"description": "Create modal dialogs without JavaScript libraries", "description": "أنشئ مربعات حوار نموذجية بدون مكتبات JavaScript",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "dialog-basic", "id": "dialog-basic",
"title": "Open Dialog", "title": "فتح مربع الحوار",
"description": "The <kbd>&lt;dialog&gt;</kbd> element creates a native modal. Add the <kbd>open</kbd> attribute to show it.<br><br>Use <kbd>&lt;form method=\"dialog\"&gt;</kbd> inside to close it when the form submits - no JavaScript needed!", "description": "عنصر <kbd>&lt;dialog&gt;</kbd> ينشئ نافذة نموذجية أصلية. أضف سمة <kbd>open</kbd> لعرضها.<br><br>استخدم <kbd>&lt;form method=\"dialog\"&gt;</kbd> بداخلها لإغلاقها عند إرسال النموذج - بدون JavaScript!",
"task": "Create a dialog with:<br>1. The <kbd>open</kbd> attribute to show it<br>2. An <kbd>&lt;h2&gt;</kbd> saying <code>Welcome!</code><br>3. A <kbd>&lt;p&gt;</kbd> with a greeting message<br>4. A <kbd>&lt;form method=\"dialog\"&gt;</kbd> with a close button", "task": "أنشئ مربع حوار مع:<br>1. سمة <kbd>open</kbd> لعرضه<br>2. عنصر <kbd>&lt;h2&gt;</kbd> يقول <code>أهلاً!</code><br>3. عنصر <kbd>&lt;p&gt;</kbd> مع رسالة ترحيب<br>4. عنصر <kbd>&lt;form method=\"dialog\"&gt;</kbd> مع زر إغلاق",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; min-height: 200px; background: #f5f5f5; } dialog { border: none; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 25px; max-width: 400px; } dialog::backdrop { background: rgba(0,0,0,0.5); } dialog h2 { margin: 0 0 15px 0; color: #1976d2; } dialog p { color: #666; margin: 0 0 20px 0; } dialog button { background: #1976d2; color: white; border: none; padding: 10px 25px; border-radius: 6px; cursor: pointer; font-size: 16px; } dialog button:hover { background: #1565c0; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; min-height: 200px; background: #f5f5f5; } dialog { border: none; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 25px; max-width: 400px; } dialog::backdrop { background: rgba(0,0,0,0.5); } dialog h2 { margin: 0 0 15px 0; color: #1976d2; } dialog p { color: #666; margin: 0 0 20px 0; } dialog button { background: #1976d2; color: white; border: none; padding: 10px 25px; border-radius: 6px; cursor: pointer; font-size: 16px; } dialog button:hover { background: #1565c0; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,35 +21,35 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog", "value": "dialog",
"message": "Add a <kbd>&lt;dialog&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;dialog&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "dialog", "attr": "open", "value": true }, "value": { "selector": "dialog", "attr": "open", "value": true },
"message": "Add the <kbd>open</kbd> attribute to show the dialog" "message": "أضف سمة <kbd>open</kbd> لعرض مربع الحوار"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog h2", "value": "dialog h2",
"message": "Add an <kbd>&lt;h2&gt;</kbd> heading inside the dialog" "message": "أضف عنوان <kbd>&lt;h2&gt;</kbd> داخل مربع الحوار"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "form[method='dialog']", "value": "form[method='dialog']",
"message": "Add a <kbd>&lt;form method=\"dialog\"&gt;</kbd> for closing" "message": "أضف <kbd>&lt;form method=\"dialog\"&gt;</kbd> للإغلاق"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog button", "value": "dialog button",
"message": "Add a close button inside the form" "message": "أضف زر إغلاق داخل النموذج"
} }
] ]
}, },
{ {
"id": "dialog-form", "id": "dialog-form",
"title": "Dialog + Form", "title": "حوار + نموذج",
"description": "Dialogs can contain full forms. The <kbd>method=\"dialog\"</kbd> makes the form close the dialog on submit instead of sending data.<br><br>This pattern is perfect for confirmation dialogs, quick inputs, or settings panels.", "description": "مربعات الحوار يمكن أن تحتوي على نماذج كاملة. <kbd>method=\"dialog\"</kbd> يجعل النموذج يغلق مربع الحوار عند الإرسال بدلاً من إرسال البيانات.<br><br>هذا النمط مثالي لحوارات التأكيد، المدخلات السريعة، أو لوحات الإعدادات.",
"task": "Create a confirmation dialog:<br>1. Add <kbd>open</kbd> to show it<br>2. An <kbd>&lt;h2&gt;</kbd> saying <code>Confirm Delete</code><br>3. A <kbd>&lt;p&gt;</kbd> asking <code>Are you sure?</code><br>4. A <kbd>&lt;form method=\"dialog\"&gt;</kbd> with Cancel and Delete buttons", "task": "أنشئ مربع حوار تأكيد:<br>1. أضف <kbd>open</kbd> لعرضه<br>2. عنصر <kbd>&lt;h2&gt;</kbd> يقول <code>تأكيد الحذف</code><br>3. عنصر <kbd>&lt;p&gt;</kbd> يسأل <code>هل أنت متأكد؟</code><br>4. عنصر <kbd>&lt;form method=\"dialog\"&gt;</kbd> مع أزرار إلغاء وحذف",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; min-height: 200px; background: linear-gradient(135deg, #fce4ec 0%, #f8bbd9 100%); } dialog { border: none; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 25px; max-width: 350px; text-align: center; } dialog h2 { margin: 0 0 10px 0; color: #c62828; } dialog p { color: #666; margin: 0 0 20px 0; } dialog form { display: flex; gap: 10px; justify-content: center; } dialog button { padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; border: none; } dialog button:first-child { background: #e0e0e0; color: #333; } dialog button:last-child { background: #c62828; color: white; } dialog button:hover { opacity: 0.9; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; min-height: 200px; background: linear-gradient(135deg, #fce4ec 0%, #f8bbd9 100%); } dialog { border: none; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 25px; max-width: 350px; text-align: center; } dialog h2 { margin: 0 0 10px 0; color: #c62828; } dialog p { color: #666; margin: 0 0 20px 0; } dialog form { display: flex; gap: 10px; justify-content: center; } dialog button { padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; border: none; } dialog button:first-child { background: #e0e0e0; color: #333; } dialog button:last-child { background: #c62828; color: white; } dialog button:hover { opacity: 0.9; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -60,22 +60,22 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog[open]", "value": "dialog[open]",
"message": "Add a <kbd>&lt;dialog&gt;</kbd> with the open attribute" "message": "أضف <kbd>&lt;dialog&gt;</kbd> مع سمة open"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog h2", "value": "dialog h2",
"message": "Add a heading to the dialog" "message": "أضف عنواناً لمربع الحوار"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "form[method='dialog']", "value": "form[method='dialog']",
"message": "Add a <kbd>&lt;form method=\"dialog\"&gt;</kbd> for the buttons" "message": "أضف <kbd>&lt;form method=\"dialog\"&gt;</kbd> للأزرار"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "dialog button", "min": 2 }, "value": { "selector": "dialog button", "min": 2 },
"message": "Add at least 2 buttons (Cancel and Confirm)" "message": "أضف على الأقل زرين (إلغاء وتأكيد)"
} }
] ]
} }

View File

@@ -1,16 +1,16 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-fieldset", "id": "html-forms-fieldset",
"title": "Fieldsets", "title": "مجموعة الحقول",
"description": "Group form controls with fieldset and legend elements", "description": "جمّع عناصر التحكم في النموذج باستخدام fieldset و legend",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "fieldset-basic", "id": "fieldset-basic",
"title": "Grouping with Fieldset", "title": "التجميع مع Fieldset",
"description": "The <kbd>&lt;fieldset&gt;</kbd> element groups related form controls together. Add a <kbd>&lt;legend&gt;</kbd> as the first child to give the group a title.<br><br>This helps with accessibility and visual organization of complex forms.", "description": "عنصر <kbd>&lt;fieldset&gt;</kbd> يجمع عناصر التحكم المرتبطة في النموذج معاً. أضف <kbd>&lt;legend&gt;</kbd> كأول عنصر فرعي لإعطاء المجموعة عنواناً.<br><br>هذا يساعد في إمكانية الوصول والتنظيم البصري للنماذج المعقدة.",
"task": "Create a form with a fieldset:<br>1. A <kbd>&lt;form&gt;</kbd> element<br>2. A <kbd>&lt;fieldset&gt;</kbd> inside<br>3. A <kbd>&lt;legend&gt;</kbd> saying <code>Personal Info</code><br>4. Two labeled inputs for name and email", "task": "أنشئ نموذجاً مع fieldset:<br>1. عنصر <kbd>&lt;form&gt;</kbd><br>2. عنصر <kbd>&lt;fieldset&gt;</kbd> بداخله<br>3. عنصر <kbd>&lt;legend&gt;</kbd> يقول <code>المعلومات الشخصية</code><br>4. حقلين مُعنونين للاسم والبريد الإلكتروني",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f0f4f8; } form { max-width: 400px; } fieldset { border: 2px solid #3498db; border-radius: 10px; padding: 20px; background: white; } legend { color: #3498db; font-weight: 600; padding: 0 10px; font-size: 1.1rem; } label { display: block; margin: 15px 0 5px; color: #333; font-weight: 500; } input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; } input:focus { outline: 2px solid #3498db; border-color: transparent; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f0f4f8; } form { max-width: 400px; } fieldset { border: 2px solid #3498db; border-radius: 10px; padding: 20px; background: white; } legend { color: #3498db; font-weight: 600; padding: 0 10px; font-size: 1.1rem; } label { display: block; margin: 15px 0 5px; color: #333; font-weight: 500; } input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; } input:focus { outline: 2px solid #3498db; border-color: transparent; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,35 +21,35 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "form", "value": "form",
"message": "Add a <kbd>&lt;form&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;form&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "fieldset", "value": "fieldset",
"message": "Add a <kbd>&lt;fieldset&gt;</kbd> inside the form" "message": "أضف <kbd>&lt;fieldset&gt;</kbd> داخل النموذج"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "legend", "value": "legend",
"message": "Add a <kbd>&lt;legend&gt;</kbd> to title your fieldset" "message": "أضف <kbd>&lt;legend&gt;</kbd> لعنونة مجموعة الحقول"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "label", "min": 2 }, "value": { "selector": "label", "min": 2 },
"message": "Add at least 2 labels" "message": "أضف على الأقل عنوانين"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "input", "min": 2 }, "value": { "selector": "input", "min": 2 },
"message": "Add at least 2 input fields" "message": "أضف على الأقل حقلي إدخال"
} }
] ]
}, },
{ {
"id": "fieldset-textarea", "id": "fieldset-textarea",
"title": "Adding Textarea", "title": "إضافة Textarea",
"description": "The <kbd>&lt;textarea&gt;</kbd> element creates a multi-line text input, perfect for longer content like messages or descriptions.<br><br>Use <kbd>rows</kbd> and <kbd>cols</kbd> attributes to set default size.", "description": "عنصر <kbd>&lt;textarea&gt;</kbd> ينشئ حقل إدخال نص متعدد الأسطر، مثالي للمحتوى الطويل مثل الرسائل أو الأوصاف.<br><br>استخدم سمات <kbd>rows</kbd> و <kbd>cols</kbd> لتعيين الحجم الافتراضي.",
"task": "Create a contact form:<br>1. A <kbd>&lt;fieldset&gt;</kbd> with <kbd>&lt;legend&gt;</kbd> <code>Contact Us</code><br>2. A labeled <kbd>&lt;input&gt;</kbd> for email<br>3. A labeled <kbd>&lt;textarea&gt;</kbd> for the message<br>4. A submit <kbd>&lt;button&gt;</kbd>", "task": "أنشئ نموذج اتصال:<br>1. عنصر <kbd>&lt;fieldset&gt;</kbd> مع <kbd>&lt;legend&gt;</kbd> <code>تواصل معنا</code><br>2. حقل <kbd>&lt;input&gt;</kbd> مُعنون للبريد الإلكتروني<br>3. حقل <kbd>&lt;textarea&gt;</kbd> مُعنون للرسالة<br>4. زر <kbd>&lt;button&gt;</kbd> للإرسال",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } form { max-width: 450px; margin: 0 auto; } fieldset { border: none; border-radius: 15px; padding: 30px; background: white; box-shadow: 0 10px 40px rgba(0,0,0,0.2); } legend { color: #667eea; font-weight: 700; padding: 0; font-size: 1.5rem; margin-bottom: 10px; } label { display: block; margin: 20px 0 8px; color: #333; font-weight: 500; } input, textarea { width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 16px; box-sizing: border-box; font-family: inherit; } input:focus, textarea:focus { outline: none; border-color: #667eea; } textarea { resize: vertical; min-height: 100px; } button { margin-top: 20px; width: 100%; padding: 14px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; } button:hover { opacity: 0.9; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } form { max-width: 450px; margin: 0 auto; } fieldset { border: none; border-radius: 15px; padding: 30px; background: white; box-shadow: 0 10px 40px rgba(0,0,0,0.2); } legend { color: #667eea; font-weight: 700; padding: 0; font-size: 1.5rem; margin-bottom: 10px; } label { display: block; margin: 20px 0 8px; color: #333; font-weight: 500; } input, textarea { width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 16px; box-sizing: border-box; font-family: inherit; } input:focus, textarea:focus { outline: none; border-color: #667eea; } textarea { resize: vertical; min-height: 100px; } button { margin-top: 20px; width: 100%; padding: 14px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; } button:hover { opacity: 0.9; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -60,35 +60,35 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "fieldset", "value": "fieldset",
"message": "Add a <kbd>&lt;fieldset&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;fieldset&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "legend", "value": "legend",
"message": "Add a <kbd>&lt;legend&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;legend&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "textarea", "value": "textarea",
"message": "Add a <kbd>&lt;textarea&gt;</kbd> for the message" "message": "أضف <kbd>&lt;textarea&gt;</kbd> للرسالة"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "button", "value": "button",
"message": "Add a submit button" "message": "أضف زر إرسال"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "input", "value": "input",
"message": "Add an input field for email" "message": "أضف حقل للبريد الإلكتروني"
} }
] ]
}, },
{ {
"id": "fieldset-multiple", "id": "fieldset-multiple",
"title": "Multiple Fieldsets", "title": "مجموعات حقول متعددة",
"description": "Complex forms can use multiple <kbd>&lt;fieldset&gt;</kbd> elements to organize different sections.<br><br>This improves usability for long forms like registration or checkout pages.", "description": "النماذج المعقدة يمكنها استخدام عناصر <kbd>&lt;fieldset&gt;</kbd> متعددة لتنظيم أقسام مختلفة.<br><br>هذا يحسن قابلية الاستخدام للنماذج الطويلة مثل التسجيل أو الدفع.",
"task": "Create a registration form with 2 fieldsets:<br>1. <code>Account Info</code> with username and password inputs<br>2. <code>Preferences</code> with a textarea for bio<br>3. A submit button outside the fieldsets", "task": "أنشئ نموذج تسجيل مع 2 مجموعات حقول:<br>1. <code>معلومات الحساب</code> مع حقول اسم المستخدم وكلمة المرور<br>2. <code>التفضيلات</code> مع textarea للسيرة الذاتية<br>3. زر إرسال خارج مجموعات الحقول",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #fafafa; } form { max-width: 500px; } fieldset { border: 1px solid #ddd; border-radius: 10px; padding: 20px; margin-bottom: 20px; background: white; } legend { color: #2c3e50; font-weight: 600; padding: 0 10px; } label { display: block; margin: 15px 0 5px; color: #555; } input, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; font-family: inherit; } input:focus, textarea:focus { outline: 2px solid #3498db; border-color: transparent; } textarea { resize: vertical; min-height: 80px; } button { width: 100%; padding: 14px; background: #2c3e50; color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; } button:hover { background: #34495e; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #fafafa; } form { max-width: 500px; } fieldset { border: 1px solid #ddd; border-radius: 10px; padding: 20px; margin-bottom: 20px; background: white; } legend { color: #2c3e50; font-weight: 600; padding: 0 10px; } label { display: block; margin: 15px 0 5px; color: #555; } input, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; font-family: inherit; } input:focus, textarea:focus { outline: 2px solid #3498db; border-color: transparent; } textarea { resize: vertical; min-height: 80px; } button { width: 100%; padding: 14px; background: #2c3e50; color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; } button:hover { background: #34495e; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -99,27 +99,27 @@
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "fieldset", "min": 2 }, "value": { "selector": "fieldset", "min": 2 },
"message": "Create at least 2 fieldsets" "message": "أنشئ على الأقل 2 مجموعات حقول"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "legend", "min": 2 }, "value": { "selector": "legend", "min": 2 },
"message": "Add a legend to each fieldset" "message": "أضف legend لكل مجموعة حقول"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "textarea", "value": "textarea",
"message": "Add a textarea for the bio" "message": "أضف textarea للسيرة الذاتية"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "button", "value": "button",
"message": "Add a submit button" "message": "أضف زر إرسال"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "input", "min": 2 }, "value": { "selector": "input", "min": 2 },
"message": "Add at least 2 input fields" "message": "أضف على الأقل حقلي إدخال"
} }
] ]
} }

View File

@@ -1,125 +1,42 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-tables", "id": "html-tables",
"title": "HTML Tables", "title": "جداول HTML",
"description": "Create structured data tables with headers and captions", "description": "أنشئ جداول بيانات منظمة بترميز دلالي",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "table-basic", "id": "table-basic",
"title": "Basic Table Structure", "title": "جداول البيانات",
"description": "Tables use <kbd>&lt;table&gt;</kbd> with <kbd>&lt;tr&gt;</kbd> for rows. Inside rows, use <kbd>&lt;th&gt;</kbd> for headers and <kbd>&lt;td&gt;</kbd> for data cells.<br><br>The <kbd>&lt;caption&gt;</kbd> element provides an accessible title for the table.", "description": "الجداول تعرض البيانات المنظمة في صفوف وأعمدة. استخدم <kbd>&lt;table&gt;</kbd> كحاوية، <kbd>&lt;tr&gt;</kbd> للصفوف، <kbd>&lt;th&gt;</kbd> لخلايا الرأس و <kbd>&lt;td&gt;</kbd> لخلايا البيانات.<br><br>أضف <kbd>&lt;caption&gt;</kbd> لعنوان قابل للوصول يصف محتوى الجدول.",
"task": "Create a simple table with:<br>1. A <kbd>&lt;caption&gt;</kbd> saying <code>Fruit Prices</code><br>2. A header row with <code>Fruit</code> and <code>Price</code> columns<br>3. At least 2 data rows", "task": "أنشئ جدول أسعار:<br>1. عنصر <kbd>&lt;caption&gt;</kbd> يقول <code>Pricing</code><br>2. صف رأس مع <code>Plan</code> و <code>Price</code><br>3. صفين بيانات لـ Basic ($9) و Pro ($29)",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px; font-weight: 600; font-size: 1.2rem; color: #333; background: #f8f9fa; } th, td { padding: 12px 20px; text-align: left; border-bottom: 1px solid #eee; } th { background: #3498db; color: white; font-weight: 500; } tr:hover { background: #f8f9fa; } tr:last-child td { border-bottom: none; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; } table { border-collapse: collapse; width: 100%; max-width: 350px; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 16px; font-weight: 600; font-size: 1.1rem; color: #333; background: #f8f9fa; } th, td { padding: 14px 20px; text-align: left; border-bottom: 1px solid #eee; } th { background: steelblue; color: white; font-weight: 500; } tr:last-child td { border-bottom: none; } tr:hover td { background: #f8f9fa; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<table>\n <caption>Fruit Prices</caption>\n <tr>\n <th>Fruit</th>\n <th>Price</th>\n </tr>\n <tr>\n <td>Apple</td>\n <td>$1.50</td>\n </tr>\n <tr>\n <td>Banana</td>\n <td>$0.75</td>\n </tr>\n</table>", "solution": "<table>\n <caption>Pricing</caption>\n <tr>\n <th>Plan</th>\n <th>Price</th>\n </tr>\n <tr>\n <td>Basic</td>\n <td>$9</td>\n </tr>\n <tr>\n <td>Pro</td>\n <td>$29</td>\n </tr>\n</table>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "table", "value": "table",
"message": "Add a <kbd>&lt;table&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;table&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "caption", "value": "caption",
"message": "Add a <kbd>&lt;caption&gt;</kbd> for the table title" "message": "أضف <kbd>&lt;caption&gt;</kbd> لعنوان الجدول"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "th", "min": 2 }, "value": { "selector": "th", "min": 2 },
"message": "Add at least 2 header cells (th)" "message": "أضف خلايا رأس (<kbd>&lt;th&gt;</kbd>) لـ Plan و Price"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "tr", "min": 3 }, "value": { "selector": "tr", "min": 3 },
"message": "Add at least 3 rows (1 header + 2 data rows)" "message": "أضف 3 صفوف (1 رأس + 2 صفوف بيانات)"
}
]
},
{
"id": "table-thead-tbody",
"title": "Table Head & Body",
"description": "Use <kbd>&lt;thead&gt;</kbd> to group header rows and <kbd>&lt;tbody&gt;</kbd> to group data rows. This helps browsers and assistive technology understand the table structure.<br><br>You can also use <kbd>&lt;tfoot&gt;</kbd> for footer rows like totals.",
"task": "Create a structured table:<br>1. A <kbd>&lt;caption&gt;</kbd> with <code>Monthly Sales</code><br>2. A <kbd>&lt;thead&gt;</kbd> with Month and Revenue headers<br>3. A <kbd>&lt;tbody&gt;</kbd> with at least 2 data rows",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } table { border-collapse: collapse; width: 100%; max-width: 450px; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.2); } caption { padding: 20px; font-weight: 700; font-size: 1.3rem; color: white; background: transparent; text-shadow: 0 2px 4px rgba(0,0,0,0.2); caption-side: top; } thead { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } th { padding: 15px 20px; text-align: left; color: white; font-weight: 500; } tbody tr { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } td { padding: 15px 20px; color: #333; } tbody tr:last-child { border-bottom: none; }",
"sandboxCSS": "",
"initialCode": "",
"solution": "<table>\n <caption>Monthly Sales</caption>\n <thead>\n <tr>\n <th>Month</th>\n <th>Revenue</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>January</td>\n <td>$12,500</td>\n </tr>\n <tr>\n <td>February</td>\n <td>$14,200</td>\n </tr>\n </tbody>\n</table>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "table",
"message": "Add a <kbd>&lt;table&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "caption",
"message": "Add a <kbd>&lt;caption&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "thead",
"message": "Add a <kbd>&lt;thead&gt;</kbd> for the header section"
},
{
"type": "element_exists",
"value": "tbody",
"message": "Add a <kbd>&lt;tbody&gt;</kbd> for the data rows"
},
{
"type": "element_count",
"value": { "selector": "tbody tr", "min": 2 },
"message": "Add at least 2 data rows in tbody"
}
]
},
{
"id": "table-complete",
"title": "Complete Table with Footer",
"description": "Add <kbd>&lt;tfoot&gt;</kbd> to create a footer section for totals or summary data. The footer stays at the bottom even if tbody has many rows.<br><br>Combine all sections for a fully structured, accessible table.",
"task": "Create a complete table:<br>1. A <kbd>&lt;caption&gt;</kbd> with <code>Order Summary</code><br>2. A <kbd>&lt;thead&gt;</kbd> with Item and Price headers<br>3. A <kbd>&lt;tbody&gt;</kbd> with 2 items<br>4. A <kbd>&lt;tfoot&gt;</kbd> with a Total row",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #fafafa; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px 20px; font-weight: 600; font-size: 1.1rem; color: #333; text-align: left; background: #f8f9fa; border-bottom: 2px solid #eee; } th, td { padding: 12px 20px; text-align: left; } thead th { background: #2c3e50; color: white; } tbody td { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } tfoot { background: #ecf0f1; font-weight: 600; } tfoot td { border-top: 2px solid #2c3e50; color: #2c3e50; }",
"sandboxCSS": "",
"initialCode": "",
"solution": "<table>\n <caption>Order Summary</caption>\n <thead>\n <tr>\n <th>Item</th>\n <th>Price</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>Widget</td>\n <td>$25.00</td>\n </tr>\n <tr>\n <td>Gadget</td>\n <td>$35.00</td>\n </tr>\n </tbody>\n <tfoot>\n <tr>\n <td>Total</td>\n <td>$60.00</td>\n </tr>\n </tfoot>\n</table>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "table",
"message": "Add a <kbd>&lt;table&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "caption",
"message": "Add a <kbd>&lt;caption&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "thead",
"message": "Add a <kbd>&lt;thead&gt;</kbd> section"
},
{
"type": "element_exists",
"value": "tbody",
"message": "Add a <kbd>&lt;tbody&gt;</kbd> section"
},
{
"type": "element_exists",
"value": "tfoot",
"message": "Add a <kbd>&lt;tfoot&gt;</kbd> section for the total"
},
{
"type": "element_count",
"value": { "selector": "tbody tr", "min": 2 },
"message": "Add at least 2 item rows in tbody"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-marquee", "id": "html-marquee",
"title": "HTML Marquee", "title": "HTML Marquee",
"description": "Create scrolling text with the classic (deprecated but fun!) marquee element", "description": "أنشئ نصاً متحركاً باستخدام عنصر marquee الكلاسيكي (قديم لكنه ممتع!)",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "marquee-basic", "id": "marquee-basic",
"title": "Scrolling Text", "title": "النص المتحرك",
"description": "The <kbd>&lt;marquee&gt;</kbd> element creates scrolling text - a classic from the early web! While deprecated, it still works in most browsers.<br><br>Note: For production, use CSS animations instead. But for learning and fun, marquee is great!", "description": "عنصر <kbd>&lt;marquee&gt;</kbd> ينشئ نصاً متحركاً - كلاسيكي من الويب القديم! رغم أنه قديم، لا يزال يعمل في معظم المتصفحات.<br><br>ملاحظة: للإنتاج، استخدم حركات CSS بدلاً منه. لكن للتعلم والمرح، marquee رائع!",
"task": "Create a simple marquee:<br>1. Add a <kbd>&lt;marquee&gt;</kbd> element<br>2. Put some text inside like <code>Welcome to my website!</code>", "task": "أنشئ marquee بسيط:<br>1. أضف عنصر <kbd>&lt;marquee&gt;</kbd><br>2. ضع نصاً بداخله مثل <code>مرحباً بك في موقعي!</code>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2rem; color: #00ff00; text-shadow: 0 0 10px #00ff00, 0 0 20px #00ff00; font-family: 'Courier New', monospace; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2rem; color: #00ff00; text-shadow: 0 0 10px #00ff00, 0 0 20px #00ff00; font-family: 'Courier New', monospace; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,15 +21,15 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "marquee", "value": "marquee",
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;marquee&gt;</kbd>"
} }
] ]
}, },
{ {
"id": "marquee-direction", "id": "marquee-direction",
"title": "Direction & Behavior", "title": "الاتجاه والسلوك",
"description": "Control the marquee with attributes:<br>• <kbd>direction</kbd>: left, right, up, down<br>• <kbd>behavior</kbd>: scroll (default), slide (stops at edge), alternate (bounces)<br>• <kbd>scrollamount</kbd>: speed (default is 6)", "description": "تحكم في marquee باستخدام السمات:<br>• <kbd>direction</kbd>: left، right، up، down<br>• <kbd>behavior</kbd>: scroll (افتراضي)، slide (يتوقف عند الحافة)، alternate (يرتد)<br>• <kbd>scrollamount</kbd>: السرعة (الافتراضي 6)",
"task": "Create a bouncing marquee:<br>1. Add a <kbd>&lt;marquee&gt;</kbd> element<br>2. Set <kbd>behavior=\"alternate\"</kbd> to make it bounce<br>3. Add some fun text", "task": "أنشئ marquee مرتداً:<br>1. أضف عنصر <kbd>&lt;marquee&gt;</kbd><br>2. اضبط <kbd>behavior=\"alternate\"</kbd> ليرتد<br>3. أضف نصاً ممتعاً",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2.5rem; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); font-weight: bold; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2.5rem; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); font-weight: bold; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -40,20 +40,20 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "marquee", "value": "marquee",
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;marquee&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "marquee", "attr": "behavior", "value": "alternate" }, "value": { "selector": "marquee", "attr": "behavior", "value": "alternate" },
"message": "Add <kbd>behavior=</kbd>\"alternate\" to make it bounce" "message": "أضف <kbd>behavior=</kbd>\"alternate\" ليرتد"
} }
] ]
}, },
{ {
"id": "marquee-retro", "id": "marquee-retro",
"title": "Retro News Ticker", "title": "شريط أخبار كلاسيكي",
"description": "Combine multiple marquee attributes for a classic news ticker effect. You can even put multiple elements inside!<br><br>Remember: This is deprecated HTML. Modern sites use CSS animations, but marquee is great for understanding web history.", "description": "اجمع عدة سمات marquee لتأثير شريط أخبار كلاسيكي. يمكنك حتى وضع عناصر متعددة بداخله!<br><br>تذكر: هذا HTML قديم. المواقع الحديثة تستخدم حركات CSS، لكن marquee رائع لفهم تاريخ الويب.",
"task": "Create a news ticker:<br>1. A <kbd>&lt;marquee&gt;</kbd> with <kbd>direction=\"left\"</kbd><br>2. Set <kbd>scrollamount=\"5\"</kbd> for smooth scrolling<br>3. Add a breaking news headline inside", "task": "أنشئ شريط أخبار:<br>1. عنصر <kbd>&lt;marquee&gt;</kbd> مع <kbd>direction=\"left\"</kbd><br>2. اضبط <kbd>scrollamount=\"5\"</kbd> للتمرير السلس<br>3. أضف عنواناً إخبارياً عاجلاً بداخله",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 0; margin: 0; background: #1a1a2e; } marquee { background: linear-gradient(90deg, #c0392b 0%, #e74c3c 50%, #c0392b 100%); padding: 15px 0; font-size: 1.3rem; color: white; font-weight: 500; text-transform: uppercase; letter-spacing: 1px; border-top: 3px solid #f1c40f; border-bottom: 3px solid #f1c40f; }", "previewBaseCSS": "body { font-family: system-ui; padding: 0; margin: 0; background: #1a1a2e; } marquee { background: linear-gradient(90deg, #c0392b 0%, #e74c3c 50%, #c0392b 100%); padding: 15px 0; font-size: 1.3rem; color: white; font-weight: 500; text-transform: uppercase; letter-spacing: 1px; border-top: 3px solid #f1c40f; border-bottom: 3px solid #f1c40f; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -64,17 +64,17 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "marquee", "value": "marquee",
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;marquee&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "marquee", "attr": "direction", "value": "left" }, "value": { "selector": "marquee", "attr": "direction", "value": "left" },
"message": "Add <kbd>direction=</kbd>\"left\" for horizontal scrolling" "message": "أضف <kbd>direction=</kbd>\"left\" للتمرير الأفقي"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "marquee", "attr": "scrollamount", "value": "5" }, "value": { "selector": "marquee", "attr": "scrollamount", "value": "5" },
"message": "Add <kbd>scrollamount=</kbd>\"5\" for smooth speed" "message": "أضف <kbd>scrollamount=</kbd>\"5\" لسرعة سلسة"
} }
] ]
} }

View File

@@ -2,99 +2,169 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-svg", "id": "html-svg",
"title": "HTML SVG", "title": "HTML SVG",
"description": "Draw scalable vector graphics directly in HTML", "description": "ارسم رسومات متجهة قابلة للتحجيم مباشرة في HTML",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "svg-circle", "id": "svg-circle",
"title": "Drawing Circles", "title": "رسم الدوائر",
"description": "SVG (Scalable Vector Graphics) lets you draw shapes directly in HTML. The <kbd>&lt;svg&gt;</kbd> element is the container, with <kbd>width</kbd> and <kbd>height</kbd> attributes.<br><br>Use <kbd>&lt;circle&gt;</kbd> with <kbd>cx</kbd>, <kbd>cy</kbd> (center) and <kbd>r</kbd> (radius) to draw circles.", "description": "SVG (Scalable Vector Graphics) يتيح لك رسم الأشكال مباشرة في HTML. عنصر <kbd>&lt;svg&gt;</kbd> هو الحاوية بسمات <kbd>width</kbd> و <kbd>height</kbd>.<br><br>استخدم <kbd>&lt;circle&gt;</kbd> مع <kbd>cx</kbd> و <kbd>cy</kbd> (المركز) و <kbd>r</kbd> (نصف القطر) لرسم الدوائر.",
"task": "Create an SVG with a circle:<br>1. An <kbd>&lt;svg&gt;</kbd> with width=\"200\" and height=\"200\"<br>2. A <kbd>&lt;circle&gt;</kbd> centered at (100,100) with radius 50<br>3. Add a <kbd>fill</kbd> color", "task": "أنشئ SVG مع دائرة:<br>1. عنصر <kbd>&lt;svg&gt;</kbd> بـ width=\"200\" و height=\"200\"<br>2. عنصر <kbd>&lt;circle&gt;</kbd> متمركز عند (100,100) بنصف قطر 50<br>3. أضف لون <kbd>fill</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; display: flex; justify-content: center; } svg { background: white; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; display: flex; justify-content: center; } svg { background: white; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"#3498db\" />\n</svg>", "solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"steelblue\" />\n</svg>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "svg", "value": "svg",
"message": "Add an <kbd>&lt;svg&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;svg&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "circle", "value": "circle",
"message": "Add a <kbd>&lt;circle&gt;</kbd> element inside the SVG" "message": "أضف عنصر <kbd>&lt;circle&gt;</kbd> داخل SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "width", "value": "200" },
"message": "عيّن <kbd>width=</kbd>\"200\" في SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "height", "value": "200" },
"message": "عيّن <kbd>height=</kbd>\"200\" في SVG"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "circle", "attr": "cx", "value": "100" }, "value": { "selector": "circle", "attr": "cx", "value": "100" },
"message": "Set <kbd>cx=</kbd>\"100\" for the circle's horizontal center" "message": "عيّن <kbd>cx=</kbd>\"100\" للمركز الأفقي للدائرة"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "circle", "attr": "cy", "value": "100" }, "value": { "selector": "circle", "attr": "cy", "value": "100" },
"message": "Set <kbd>cy=</kbd>\"100\" for the circle's vertical center" "message": "عيّن <kbd>cy=</kbd>\"100\" للمركز العمودي للدائرة"
},
{
"type": "attribute_value",
"value": { "selector": "circle", "attr": "r", "value": "50" },
"message": "عيّن <kbd>r=</kbd>\"50\" لنصف قطر الدائرة"
} }
] ]
}, },
{ {
"id": "svg-rect-line", "id": "svg-rect-line",
"title": "Rectangles & Lines", "title": "المستطيلات والخطوط",
"description": "Draw rectangles with <kbd>&lt;rect&gt;</kbd> using <kbd>x</kbd>, <kbd>y</kbd>, <kbd>width</kbd>, <kbd>height</kbd>.<br><br>Draw lines with <kbd>&lt;line&gt;</kbd> using <kbd>x1</kbd>, <kbd>y1</kbd> (start) and <kbd>x2</kbd>, <kbd>y2</kbd> (end). Lines need a <kbd>stroke</kbd> color!", "description": "ارسم المستطيلات باستخدام <kbd>&lt;rect&gt;</kbd> مع <kbd>x</kbd> و <kbd>y</kbd> و <kbd>width</kbd> و <kbd>height</kbd>.<br><br>ارسم الخطوط باستخدام <kbd>&lt;line&gt;</kbd> مع <kbd>x1</kbd> و <kbd>y1</kbd> (البداية) و <kbd>x2</kbd> و <kbd>y2</kbd> (النهاية). الخطوط تحتاج لون <kbd>stroke</kbd>!",
"task": "Create an SVG with:<br>1. An <kbd>&lt;svg&gt;</kbd> (200x150)<br>2. A <kbd>&lt;rect&gt;</kbd> at position (20,20) with size 80x60<br>3. A <kbd>&lt;line&gt;</kbd> from (120,30) to (180,90) with a stroke color", "task": "أنشئ SVG مع:<br>1. عنصر <kbd>&lt;svg&gt;</kbd> (200x150)<br>2. عنصر <kbd>&lt;rect&gt;</kbd> في الموضع (20,20) بحجم 80x60<br>3. عنصر <kbd>&lt;line&gt;</kbd> من (120,30) إلى (180,90) مع لون stroke",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 200px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 200px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<svg width=\"200\" height=\"150\">\n <rect x=\"20\" y=\"20\" width=\"80\" height=\"60\" fill=\"#e74c3c\" />\n <line x1=\"120\" y1=\"30\" x2=\"180\" y2=\"90\" stroke=\"#2c3e50\" stroke-width=\"3\" />\n</svg>", "solution": "<svg width=\"200\" height=\"150\">\n <rect x=\"20\" y=\"20\" width=\"80\" height=\"60\" fill=\"tomato\" />\n <line x1=\"120\" y1=\"30\" x2=\"180\" y2=\"90\" stroke=\"slategray\" stroke-width=\"3\" />\n</svg>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "svg", "value": "svg",
"message": "Add an <kbd>&lt;svg&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;svg&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "rect", "value": "rect",
"message": "Add a <kbd>&lt;rect&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;rect&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "line", "value": "line",
"message": "Add a <kbd>&lt;line&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;line&gt;</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "width", "value": "200" },
"message": "عيّن <kbd>width=</kbd>\"200\" في SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "height", "value": "150" },
"message": "عيّن <kbd>height=</kbd>\"150\" في SVG"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "x", "value": "20" },
"message": "عيّن <kbd>x=</kbd>\"20\" في rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "y", "value": "20" },
"message": "عيّن <kbd>y=</kbd>\"20\" في rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "width", "value": "80" },
"message": "عيّن <kbd>width=</kbd>\"80\" في rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "height", "value": "60" },
"message": "عيّن <kbd>height=</kbd>\"60\" في rect"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "x1", "value": "120" },
"message": "عيّن <kbd>x1=</kbd>\"120\" في line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "y1", "value": "30" },
"message": "عيّن <kbd>y1=</kbd>\"30\" في line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "x2", "value": "180" },
"message": "عيّن <kbd>x2=</kbd>\"180\" في line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "y2", "value": "90" },
"message": "عيّن <kbd>y2=</kbd>\"90\" في line"
},
{
"type": "contains",
"value": "stroke",
"message": "أضف لون <kbd>stroke</kbd> إلى line"
} }
] ]
}, },
{ {
"id": "svg-shapes", "id": "svg-shapes",
"title": "Multiple Shapes", "title": "أشكال متعددة",
"description": "Combine shapes to create simple graphics! Add <kbd>stroke</kbd> for outlines and <kbd>stroke-width</kbd> for thickness.<br><br>Use <kbd>fill=\"none\"</kbd> for hollow shapes. Shapes stack in order - later elements appear on top.", "description": "ادمج الأشكال لإنشاء رسومات بسيطة! أضف <kbd>stroke</kbd> للحدود و <kbd>stroke-width</kbd> للسمك.<br><br>استخدم <kbd>fill=\"none\"</kbd> للأشكال المجوفة. الأشكال تتراكم بالترتيب - العناصر اللاحقة تظهر في الأعلى.",
"task": "Create a simple face:<br>1. An <kbd>&lt;svg&gt;</kbd> (200x200)<br>2. A large <kbd>&lt;circle&gt;</kbd> for the face<br>3. Two small <kbd>&lt;circle&gt;</kbd> elements for eyes<br>4. A <kbd>&lt;line&gt;</kbd> for the smile", "task": "أنشئ وجهاً بسيطاً:<br>1. عنصر <kbd>&lt;svg&gt;</kbd> (200x200)<br>2. <kbd>&lt;circle&gt;</kbd> كبير للوجه<br>3. اثنين <kbd>&lt;circle&gt;</kbd> صغيرين للعيون<br>4. عنصر <kbd>&lt;line&gt;</kbd> للابتسامة",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); min-height: 250px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); min-height: 250px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"80\" fill=\"#f1c40f\" stroke=\"#e67e22\" stroke-width=\"4\" />\n <circle cx=\"70\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <circle cx=\"130\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <line x1=\"60\" y1=\"130\" x2=\"140\" y2=\"130\" stroke=\"#2c3e50\" stroke-width=\"4\" stroke-linecap=\"round\" />\n</svg>", "solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"80\" fill=\"gold\" stroke=\"orange\" stroke-width=\"4\" />\n <circle cx=\"70\" cy=\"80\" r=\"10\" fill=\"darkslategray\" />\n <circle cx=\"130\" cy=\"80\" r=\"10\" fill=\"darkslategray\" />\n <line x1=\"60\" y1=\"130\" x2=\"140\" y2=\"130\" stroke=\"darkslategray\" stroke-width=\"4\" stroke-linecap=\"round\" />\n</svg>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "svg", "value": "svg",
"message": "Add an <kbd>&lt;svg&gt;</kbd> element" "message": "أضف عنصر <kbd>&lt;svg&gt;</kbd>"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "circle", "min": 3 }, "value": { "selector": "circle", "min": 3 },
"message": "Add at least 3 circles (1 face + 2 eyes)" "message": "أضف على الأقل 3 دوائر (1 للوجه + 2 للعيون)"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "line", "value": "line",
"message": "Add a <kbd>&lt;line&gt;</kbd> for the smile" "message": "أضف <kbd>&lt;line&gt;</kbd> للابتسامة"
} }
] ]
} }

View File

@@ -2,18 +2,18 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "box-model", "id": "box-model",
"title": "CSS Box Model", "title": "CSS Box Model",
"description": "Beherrsche die Grundprinzipien der Raumverwaltung im Webdesign durch das CSS Box-Modell.", "description": "Beherrsche die Grundprinzipien der Raumverwaltung im Webdesign durch das CSS Box-Modell. Dieses Modul erkundet, wie Inhalt, Padding, Rahmen und Margins zusammenwirken, um Layout-Strukturen zu erstellen.",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "box-model-1", "id": "box-model-1",
"title": "Box-Modell Komponenten", "title": "Padding",
"description": "Das CSS Box-Modell besteht aus vier konzentrischen Schichten: Inhalt (innerste), Padding, Border und Margin (äußerste). Zu verstehen, wie diese Komponenten zusammenwirken, ist essentiell für präzise Layout-Kontrolle.", "description": "Jedes Element in CSS ist eine Box mit vier Schichten: Inhalt, Padding, Rahmen und Margin. <strong>Padding</strong> schafft Freiraum zwischen deinem Inhalt und dem Rand der Box.<br><br>Ohne Padding drückt sich Text unangenehm gegen Rahmen. Padding macht Inhalte lesbar und visuell ausgewogen.<br><br><pre>.card {\n padding: 1rem;\n}</pre>",
"task": "Setze <kbd>padding</kbd> auf <kbd>1rem</kbd>, um Abstand zwischen Inhalt und Rahmen zu schaffen.", "task": "Diese Profilkarte sieht beengt aus. Füge <kbd>padding: 1rem</kbd> hinzu, damit der Text Platz zum Atmen hat.",
"previewHTML": "<div class=\"box\">Box-Modell Komponenten</div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: lavender; border: 2px dashed slategray; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".box {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "padding: 1rem;", "solution": "padding: 1rem;",
@@ -28,56 +28,56 @@
}, },
{ {
"id": "box-model-2", "id": "box-model-2",
"title": "Rahmen hinzufügen", "title": "Borders",
"description": "Rahmen umranden ein Element und schaffen visuelle Trennung. Die border-Kurzschreibweise akzeptiert drei Werte: Breite, Stil und Farbe.", "description": "Rahmen erstellen visuelle Grenzen um Elemente. Die <kbd>border</kbd>-Kurzschreibweise akzeptiert drei Werte: Breite, Stil und Farbe.<br><br>Häufige Stile: <kbd>solid</kbd>, <kbd>dashed</kbd>, <kbd>dotted</kbd>, <kbd>none</kbd>",
"task": "Setze <kbd>border</kbd> auf <kbd>2px solid darkslategray</kbd>.", "task": "Füge der Karte einen dezenten linken Akzent hinzu mit <kbd>border-left: 4px solid steelblue</kbd>.",
"previewHTML": "<div class=\"box\">Diese Box braucht einen Rahmen</div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: mintcream; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".box {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "border: 2px solid darkslategray;", "solution": "border-left: 4px solid steelblue;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "regex", "type": "regex",
"value": "border:\\s*2px\\s+solid\\s+darkslategray", "value": "border-left:\\s*4px\\s+solid\\s+steelblue",
"message": "Setze <kbd>border: 2px solid darkslategray</kbd>", "message": "Setze <kbd>border-left: 4px solid steelblue</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "box-model-3", "id": "box-model-3",
"title": "Außenabstände hinzufügen", "title": "Margins",
"description": "Margins schaffen Abstand zwischen Elementen. Anders als Padding (das den inneren Abstand beeinflusst) existiert Margin außerhalb des Rahmens.", "description": "Margins schaffen Abstand <em>außerhalb</em> des Elements und trennen es von Nachbarn. Während Padding den Inhalt nach innen drückt, drücken Margins andere Elemente weg.",
"task": "Setze <kbd>margin</kbd> auf <kbd>1rem</kbd>, um Abstand zum benachbarten Element zu schaffen.", "task": "Füge Abstand zwischen diesen beiden Profilkarten hinzu mit <kbd>margin-bottom: 1rem</kbd> auf <kbd>.card</kbd>.",
"previewHTML": "<div class=\"container\"><div class=\"outer\">Diese Box braucht Margins</div><div class=\"neighbor\">Benachbartes Element</div></div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article><article class=\"card\"><h3>Alex Rivera</h3><p>UX Designer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .outer { background-color: plum; padding: 1rem; border: 2px solid orchid; } .neighbor { background-color: lightblue; padding: 1rem; border: 2px solid steelblue; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".outer {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "margin: 1rem;", "solution": "margin-bottom: 1rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "margin", "expected": "1rem" }, "value": { "property": "margin-bottom", "expected": "1rem" },
"message": "Setze <kbd>margin: 1rem</kbd>" "message": "Setze <kbd>margin-bottom: 1rem</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-4", "id": "box-model-4",
"title": "Box-Sizing: Border-Box", "title": "Box Sizing",
"description": "Die box-sizing Eigenschaft bestimmt, wie Elementdimensionen berechnet werden. 'content-box' (Standard) schließt Padding und Border aus, während 'border-box' sie einschließt.", "description": "Standardmäßig setzt <kbd>width</kbd> nur die Inhaltsbreite. Padding und Rahmen werden addiert. Das verursacht Layout-Probleme.<br><br><kbd>box-sizing: border-box</kbd> bezieht Padding und Rahmen in die Breite ein, was das Sizing vorhersehbar macht. Die meisten Entwickler wenden dies auf alle Elemente an.",
"task": "Setze <kbd>box-sizing</kbd> auf <kbd>border-box</kbd>, damit Padding und Border in die Breite einbezogen werden.", "task": "Beide Karten haben <kbd>width: 200px</kbd>. Die linke nutzt Standard-Sizing (content-box) und wird breiter als erwartet. Korrigiere die rechte Karte mit <kbd>box-sizing: border-box</kbd>.",
"previewHTML": "<div class=\"sizing-demo\"><div class=\"box default\">Content-box (Standard)</div><div class=\"box sized\">Border-box</div></div>", "previewHTML": "<div class=\"demo\"><article class=\"card\">Content-box</article><article class=\"card fix\">Border-box</article></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .sizing-demo { display: flex; gap: 1rem; } .box { width: 200px; padding: 1rem; border: 4px solid teal; background: lightcyan; } .default { box-sizing: content-box; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .demo { display: flex; gap: 1rem; } .card { width: 200px; padding: 1rem; border: 4px solid steelblue; background: white; border-radius: 8px; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".sized {\n ", "codePrefix": ".fix {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "box-sizing: border-box;", "solution": "box-sizing: border-box;",
@@ -92,87 +92,98 @@
}, },
{ {
"id": "box-model-5", "id": "box-model-5",
"title": "Margin-Kollaps", "title": "Padding Shorthand",
"description": "Wenn zwei vertikale Margins aufeinandertreffen, kollabieren sie zum größeren der beiden Werte statt zu addieren.", "description": "Padding akzeptiert 1-4 Werte:<br>• 1 Wert: alle Seiten<br>• 2 Werte: vertikal | horizontal<br>• 4 Werte: oben | rechts | unten | links",
"task": "Setze <kbd>margin-bottom</kbd> auf <kbd>2rem</kbd>. Der Abstand zwischen den Absätzen beträgt 2rem (nicht 3rem) durch Margin-Kollaps.", "task": "Dieser Button braucht mehr horizontalen als vertikalen Platz. Setze <kbd>padding: 8px 1rem</kbd> (8px oben/unten, 1rem links/rechts).",
"previewHTML": "<div class=\"collapse-demo\"><p class=\"first\">Dieser Absatz hat einen Bottom-Margin.</p><p class=\"second\">Dieser Absatz hat einen Top-Margin von 1rem.</p></div>", "previewHTML": "<button class=\"btn\">Follow</button>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .collapse-demo { border: 1px solid silver; padding: 8px; background: ghostwhite; } .second { margin-top: 1rem; background: linen; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".first {\n ", "codePrefix": ".btn {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "margin-bottom: 2rem;", "solution": "padding: 8px 1rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "regex",
"value": { "property": "margin-bottom", "expected": "2rem" }, "value": "padding:\\s*8px\\s+1rem",
"message": "Setze <kbd>margin-bottom: 2rem</kbd>" "message": "Setze <kbd>padding: 8px 1rem</kbd>",
"options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "box-model-6", "id": "box-model-6",
"title": "Margin-Kurzschreibweise", "title": "Margin Shorthand",
"description": "Die Margin-Kurzschreibweise kann alle vier Seiten setzen. Zwei Werte setzen vertikale (oben/unten) und horizontale (links/rechts) Margins.", "description": "Margin nutzt das gleiche Kurzschreibweisen-Muster wie Padding. Ein häufiges Muster ist das horizontale Zentrieren von Block-Elementen mit <kbd>margin: 0 auto</kbd>.",
"task": "Setze <kbd>margin</kbd> auf <kbd>1rem 2rem</kbd> für 1rem oben/unten und 2rem links/rechts.", "task": "Zentriere diese Karte horizontal. Setze <kbd>margin: 0 auto</kbd>, um automatisch gleiche links/rechts-Margins zu berechnen.",
"previewHTML": "<div class=\"container\"><div class=\"spaced\">Diese Box braucht Margins: 1rem oben/unten, 2rem links/rechts</div></div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .spaced { background-color: honeydew; border: 2px solid mediumseagreen; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { width: 250px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".spaced {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "margin: 1rem 2rem;", "solution": "margin: 0 auto;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "regex", "type": "regex",
"value": "margin:\\s*1rem\\s+2rem", "value": "margin:\\s*0\\s+auto",
"message": "Setze <kbd>margin: 1rem 2rem</kbd>", "message": "Setze <kbd>margin: 0 auto</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "box-model-7", "id": "box-model-7",
"title": "Padding-Kurzschreibweise", "title": "Border Radius",
"description": "Wie Margin erlaubt Padding-Kurzschreibweise das Setzen aller Seiten. Ein einzelner Wert gilt für alle vier Seiten.", "description": "Obwohl nicht Teil des klassischen Box-Modells, rundet <kbd>border-radius</kbd> die Ecken der Rahmen-Box eines Elements. Verwende <kbd>50%</kbd> bei einem quadratischen Element, um einen Kreis zu erstellen.",
"task": "Setze <kbd>padding</kbd> auf <kbd>2rem</kbd> für gleichmäßiges Padding.", "task": "Mache das Avatar-Bild rund mit <kbd>border-radius: 50%</kbd>.",
"previewHTML": "<div class=\"padded\">Diese Box braucht gleichmäßiges Padding</div>", "previewHTML": "<article class=\"card\"><img class=\"avatar\" src=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='35' r='25' fill='%23666'/%3E%3Ccircle cx='50' cy='90' r='40' fill='%23666'/%3E%3C/svg%3E\" alt=\"Avatar\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .padded { background-color: papayawhip; border: 2px solid orange; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; text-align: center; } .avatar { width: 80px; height: 80px; background: #ddd; margin-bottom: 8px; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".padded {\n ", "codePrefix": ".avatar {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "padding: 2rem;", "solution": "border-radius: 50%;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "padding", "expected": "2rem" }, "value": { "property": "border-radius", "expected": "50%" },
"message": "Setze <kbd>padding: 2rem</kbd>" "message": "Setze <kbd>border-radius: 50%</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-8", "id": "box-model-8",
"title": "Rahmen auf einzelnen Seiten", "title": "Complete Card",
"description": "Für feinere Kontrolle können einzelne Seiten mit border-top, border-right, border-bottom oder border-left angesprochen werden.", "description": "Kombinieren wir alles. Diese Benachrichtigungskarte braucht Styling, um professionell auszusehen.",
"task": "Setze <kbd>border-bottom</kbd> auf <kbd>4px solid dodgerblue</kbd>.", "task": "Style die Benachrichtigung: füge <kbd>padding: 1rem</kbd>, <kbd>border-left: 4px solid coral</kbd> und <kbd>border-radius: 4px</kbd> hinzu.",
"previewHTML": "<div class=\"line\">Dieses Element braucht nur einen unteren Rahmen</div>", "previewHTML": "<div class=\"alert\"><strong>New message</strong><p>You have 3 unread notifications</p></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .line { padding: 1rem; background-color: aliceblue; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { background: seashell; } .alert strong { color: coral; } .alert p { margin: 4px 0 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".line {\n ", "codePrefix": ".alert {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "border-bottom: 4px solid dodgerblue;", "solution": "padding: 1rem;\n border-left: 4px solid coral;\n border-radius: 4px;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{
"type": "property_value",
"value": { "property": "padding", "expected": "1rem" },
"message": "Setze <kbd>padding: 1rem</kbd>"
},
{ {
"type": "regex", "type": "regex",
"value": "border-bottom:\\s*4px\\s+solid\\s+dodgerblue", "value": "border-left:\\s*4px\\s+solid\\s+coral",
"message": "Setze <kbd>border-bottom: 4px solid dodgerblue</kbd>", "message": "Setze <kbd>border-left: 4px solid coral</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "border-radius", "expected": "4px" },
"message": "Setze <kbd>border-radius: 4px</kbd>"
} }
] ]
} }

View File

@@ -7,119 +7,94 @@
"lessons": [ "lessons": [
{ {
"id": "units-1", "id": "units-1",
"title": "Absolute vs. Relative Einheiten", "title": "Relative Units",
"description": "Lerne den Unterschied zwischen px, rem, em, % und vw/vh für flexible, responsive Layouts.", "description": "CSS bietet zwei Arten von Einheiten: <em>absolute</em> (wie <kbd>px</kbd>) und <em>relative</em> (wie <kbd>%</kbd> und <kbd>rem</kbd>). Relative Einheiten passen sich ihrem Kontext an und machen Layouts flexibel und zugänglich.<br><br><strong>Häufige relative Einheiten:</strong><br>• <kbd>%</kbd> Relativ zum Elternelement<br>• <kbd>rem</kbd> Relativ zur Root-Schriftgröße (typisch 16px)<br>• <kbd>em</kbd> Relativ zur Schriftgröße des Elements<br><br>Ein häufiges Muster für lesbaren Inhalt: setze <kbd>width: 100%</kbd>, damit es den verfügbaren Platz füllt, dann <kbd>max-width: 40rem</kbd> um die Zeilenlänge für Lesbarkeit zu begrenzen.",
"task": "Setze die Breite von <kbd>.box</kbd> auf <kbd>80%</kbd> und max-width auf <kbd>37.5rem</kbd>.", "task": "Dieser Artikeltext läuft auf großen Bildschirmen zu breit. Füge <kbd>max-width: 40rem</kbd> hinzu für optimale Lesebreite.",
"previewHTML": "<div class=\"box\">Ändere meine Größe!</div>", "previewHTML": "<article class=\"article\"><h2>The Art of Typography</h2><p>Good typography is invisible. When text is set well, readers absorb information without noticing the design decisions that make it comfortable to read. Line length is crucial—too wide and eyes get lost, too narrow and reading becomes choppy.</p><p>The ideal line length is 45-75 characters per line. At typical font sizes, this works out to roughly 40rem maximum width.</p></article>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { background: #f5f5f5; padding: 1rem; }", "previewBaseCSS": "body { font-family: Georgia, serif; padding: 1rem; background: #f9f9f9; } .article { background: white; padding: 2rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .article h2 { margin: 0 0 1rem; color: #333; } .article p { margin: 0 0 1rem; line-height: 1.6; color: #444; } .article p:last-child { margin-bottom: 0; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Setze flexible Größen */\n.box {", "codePrefix": ".article {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": " width: 80%;\n max-width: 37.5rem;", "solution": "max-width: 40rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{
"type": "contains",
"value": "width",
"message": "Verwende die <kbd>width</kbd> Eigenschaft",
"options": { "caseSensitive": false }
},
{ "type": "property_value", "value": { "property": "width", "expected": "80%" }, "message": "Setze width auf <kbd>80%</kbd>" },
{
"type": "contains",
"value": "max-width",
"message": "Verwende die <kbd>max-width</kbd> Eigenschaft",
"options": { "caseSensitive": false }
},
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "max-width", "expected": "37.5rem" }, "value": { "property": "max-width", "expected": "40rem" },
"message": "Setze max-width auf <kbd>37.5rem</kbd>" "message": "Setze <kbd>max-width: 40rem</kbd>"
} }
] ]
}, },
{ {
"id": "units-2", "id": "units-2",
"title": "CSS Custom Properties", "title": "CSS Variables",
"description": "Definiere und verwende Variablen (--custom properties) wieder, um deine Theme-Werte zu zentralisieren.", "description": "CSS Custom Properties (Variablen) erlauben dir, wiederverwendbare Werte zu definieren. Definiere sie mit <kbd>--name</kbd> und verwende sie mit <kbd>var(--name)</kbd>. Variablen auf <kbd>:root</kbd> sind überall verfügbar.",
"task": "Erstelle eine <code>--main-color</code> Variable in :root mit <kbd>#6200ee</kbd> und wende sie als Rahmenfarbe auf <kbd>.themed</kbd> an.", "task": "Definiere <kbd>--brand: steelblue</kbd> in <kbd>:root</kbd>, dann verwende es als <kbd>background</kbd>-Farbe für <kbd>.btn</kbd>.",
"previewHTML": "<div class=\"themed\">Variablen-Box</div>", "previewHTML": "<div class=\"actions\"><button class=\"btn\">Subscribe</button><button class=\"btn\">Learn More</button></div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .themed { padding: 1rem; border: 0.125rem solid #ddd; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .actions { display: flex; gap: 1rem; } .btn { color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 1rem; cursor: pointer; background: #ccc; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Definiere und verwende eine CSS-Variable */\n:root {", "codePrefix": ":root {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}\n.themed { }", "codeSuffix": "\n}\n\n.btn {\n background: var(--brand);\n}",
"solution": " --main-color: #6200ee;\n}\n.themed {\n border-color: var(--main-color);", "solution": "--brand: steelblue;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "contains", "type": "contains",
"value": "--main-color", "value": "--brand",
"message": "Definiere <kbd>--main-color</kbd> in :root", "message": "Definiere die <kbd>--brand</kbd> Variable",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "contains", "type": "contains",
"value": "var(--main-color)", "value": "steelblue",
"message": "Verwende <kbd>var(--main-color)</kbd>", "message": "Setze den Wert auf <kbd>steelblue</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "border", "expected": "var(--main-color)" },
"message": "Wende die Variable auf die Rahmenfarbe an",
"options": { "exact": false }
} }
] ]
}, },
{ {
"id": "units-3", "id": "units-3",
"title": "Einheiten-Berechnungen (calc)", "title": "calc() Function",
"description": "Verwende die <code>calc()</code> Funktion, um verschiedene Einheiten in einem Ausdruck zu kombinieren.", "description": "Die <kbd>calc()</kbd> Funktion ermöglicht das Mischen verschiedener Einheiten in Berechnungen. Das ist essenziell für Layouts, die feste und flexible Größen kombinieren, wie ein Sidebar-Layout.",
"task": "Setze die Breite von <kbd>.sized</kbd> auf <kbd>calc(100% - 2rem)</kbd> und min-height auf <kbd>calc(10vh + 1rem)</kbd>.", "task": "Der Hauptinhalt soll den verbleibenden Platz nach der 200px Sidebar füllen. Setze <kbd>width: calc(100% - 200px)</kbd> auf <kbd>.main</kbd>.",
"previewHTML": "<div class=\"sized\">Calc Demo</div>", "previewHTML": "<div class=\"layout\"><aside class=\"sidebar\">Sidebar<br>Navigation</aside><main class=\"main\"><h2>Main Content</h2><p>This area should fill the remaining width after accounting for the fixed-width sidebar.</p></main></div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sized { background: #e8f5e9; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .layout { display: flex; gap: 1rem; } .sidebar { width: 200px; background: #1a1a2e; color: white; padding: 1rem; border-radius: 8px; flex-shrink: 0; } .main { background: white; padding: 1rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .main h2 { margin: 0 0 8px; } .main p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Verwende calc für dynamische Größen */\n.sized {", "codePrefix": ".main {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": " width: calc(100% - 2rem);\n min-height: calc(10vh + 1rem);", "solution": "width: calc(100% - 200px);",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ "type": "contains", "value": "calc", "message": "Verwende die <kbd>calc()</kbd> Funktion", "options": { "caseSensitive": false } },
{ {
"type": "regex", "type": "regex",
"value": "width:\\s*calc\\(100% - 2rem\\)", "value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
"message": "Width sollte calc(100% - 2rem) sein", "message": "Setze <kbd>width: calc(100% - 200px)</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "regex",
"value": "min-height:\\s*calc\\(10vh \\+ 1rem\\)",
"message": "Min-height sollte calc(10vh + 1rem) sein",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "units-4", "id": "units-4",
"title": "Viewport & Responsive Einheiten", "title": "Viewport Units",
"description": "Steuere Layouts relativ zur Viewport-Größe mit vw, vh und vmin/vmax Einheiten.", "description": "Viewport-Einheiten dimensionieren Elemente relativ zum Browserfenster:<br>• <kbd>vw</kbd> 1% der Viewport-Breite<br>• <kbd>vh</kbd> 1% der Viewport-Höhe<br><br>Diese sind perfekt für Vollbild-Sektionen wie Hero-Banner.",
"task": "Gib <kbd>.view</kbd> eine Breite von <kbd>50vw</kbd> und Höhe von <kbd>20vh</kbd>.", "task": "Mache diese Hero-Sektion so hoch wie das Viewport mit <kbd>min-height: 100vh</kbd>.",
"previewHTML": "<div class=\"view\">Viewport-Box</div>", "previewHTML": "<section class=\"hero\"><h1>Welcome</h1><p>Scroll down to explore</p></section>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .view { background: #ffe0b2; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .hero { background: linear-gradient(135deg, #1a1a2e 0%, steelblue 100%); color: white; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 2rem; } .hero h1 { margin: 0 0 1rem; font-size: 2.5rem; } .hero p { margin: 0; opacity: 0.8; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Verwende Viewport-Einheiten */\n.view {", "codePrefix": ".hero {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": " width: 50vw;\n height: 20vh;", "solution": "min-height: 100vh;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ "type": "contains", "value": "vw", "message": "Verwende <kbd>vw</kbd> Einheit", "options": { "caseSensitive": false } }, {
{ "type": "contains", "value": "vh", "message": "Verwende <kbd>vh</kbd> Einheit", "options": { "caseSensitive": false } }, "type": "property_value",
{ "type": "property_value", "value": { "property": "width", "expected": "50vw" }, "message": "Setze width auf <kbd>50vw</kbd>" }, "value": { "property": "min-height", "expected": "100vh" },
{ "type": "property_value", "value": { "property": "height", "expected": "20vh" }, "message": "Setze height auf <kbd>20vh</kbd>" } "message": "Setze <kbd>min-height: 100vh</kbd>"
}
] ]
} }
] ]

View File

@@ -7,13 +7,13 @@
"lessons": [ "lessons": [
{ {
"id": "transitions-1", "id": "transitions-1",
"title": "Einfache Transitions", "title": "Transitions",
"description": "Lerne, wie du <kbd>transition</kbd> auf Eigenschaften anwendest für sanfte Änderungen bei Zustandswechseln.<br><br><pre>transition: property duration;\n/* z.B. transition: background-color 0.3s; */</pre>", "description": "Lerne, wie du <kbd>transition</kbd> auf Eigenschaften anwendest für sanfte Änderungen bei Zustandswechseln.<br><br><pre>transition: property duration;\n/* z.B. transition: background-color 0.3s; */</pre>",
"task": "Füge <kbd>transition: background-color 0.3s</kbd> zu <kbd>.btn</kbd> hinzu, damit die Farbe beim Hover sanft überblendet.", "task": "Füge <kbd>transition: background-color 0.3s</kbd> hinzu, damit die Farbe beim Hover sanft überblendet.",
"previewHTML": "<button class=\"btn\">Hover mich</button>", "previewHTML": "<button class=\"btn\">Hover Me</button>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: #6200ee; color: white; padding: 0.5rem 1rem; border: none; } .btn:hover { background: #3700b3; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: black; color: white; padding: 0.5rem 1rem; border: none; cursor: pointer; } .btn:hover { background: white; color: black; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Füge Transition hinzu */\n.btn {", "codePrefix": "/* Add transition */\n.btn {",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "}",
"solution": " transition: background-color 0.3s;", "solution": " transition: background-color 0.3s;",
@@ -35,13 +35,13 @@
}, },
{ {
"id": "transitions-2", "id": "transitions-2",
"title": "Transition Timing-Funktionen", "title": "Timing Funcs",
"description": "Erkunde Easing-Funktionen wie <kbd>ease</kbd>, <kbd>linear</kbd>, <kbd>ease-in</kbd>, <kbd>ease-out</kbd>, um das Animationstempo zu steuern.", "description": "Erkunde Easing-Funktionen wie <kbd>ease</kbd>, <kbd>linear</kbd>, <kbd>ease-in</kbd>, <kbd>ease-out</kbd>, um das Animationstempo zu steuern.",
"task": "Setze <kbd>transition-timing-function</kbd> auf <kbd>ease-in-out</kbd> bei <kbd>.btn</kbd>.", "task": "Setze <kbd>transition-timing-function</kbd> auf <kbd>ease-in-out</kbd>.",
"previewHTML": "<button class=\"btn\">Timing</button>", "previewHTML": "<button class=\"btn\">Timing</button>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: #6200ee; color: white; padding: 0.5rem 1rem; border: none; transition: background-color 0.3s; } .btn:hover { background: #03dac6; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: navy; color: gold; padding: 0.5rem 1rem; border: none; cursor: pointer; transition: all 0.5s; } .btn:hover { background: gold; color: navy; transform: scale(1.1); }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Setze Timing-Funktion */\n.btn {", "codePrefix": "/* Set timing function */\n.btn {",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "}",
"solution": " transition-timing-function: ease-in-out;", "solution": " transition-timing-function: ease-in-out;",
@@ -56,19 +56,19 @@
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "transition-timing-function", "expected": "ease-in-out" }, "value": { "property": "transition-timing-function", "expected": "ease-in-out" },
"message": "Setze <kbd>transition-timing-function: ease-in-out</kbd>" "message": "Setze timing auf <kbd>ease-in-out</kbd>"
} }
] ]
}, },
{ {
"id": "transitions-3", "id": "transitions-3",
"title": "Keyframe-Animationen Grundlagen", "title": "Keyframes",
"description": "Erstelle benannte Animationen mit <kbd>@keyframes</kbd> und wende sie mit der <kbd>animation</kbd> Kurzschreibweise an.<br><br><pre>@keyframes bounce {\n 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;\n}</pre>", "description": "Erstelle benannte Animationen mit <kbd>@keyframes</kbd> und wende sie mit der <kbd>animation</kbd> Kurzschreibweise an.<br><br><pre>@keyframes bounce {\n 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;\n}</pre>",
"task": "Definiere bei <kbd>50%</kbd> ein <kbd>transform: translateY(-20px)</kbd> und wende <kbd>animation: bounce 1s infinite</kbd> auf <kbd>.ball</kbd> an.", "task": "Definiere bei <kbd>50%</kbd> ein <kbd>transform: translateY(-20px)</kbd> und wende <kbd>animation: bounce 1s infinite</kbd> auf <kbd>.ball</kbd> an.",
"previewHTML": "<div class=\"ball\"></div>", "previewHTML": "<div class=\"ball\"></div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .ball { width: 50px; height: 50px; background: #ff0266; border-radius: 50%; margin: 2rem auto; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .ball { width: 50px; height: 50px; background: crimson; border-radius: 50%; margin: 2rem auto; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Definiere Keyframes und wende Animation an */\n@keyframes bounce {", "codePrefix": "/* Define keyframes and apply animation */\n@keyframes bounce {",
"initialCode": "", "initialCode": "",
"codeSuffix": "}\n.ball { }", "codeSuffix": "}\n.ball { }",
"solution": " 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;", "solution": " 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;",
@@ -102,35 +102,22 @@
}, },
{ {
"id": "transitions-4", "id": "transitions-4",
"title": "Animations-Eigenschaften im Detail", "title": "Animation Properties",
"description": "Verfeinere Animationen mit <kbd>animation-delay</kbd>, <kbd>animation-iteration-count</kbd>, <kbd>animation-direction</kbd> und <kbd>animation-fill-mode</kbd>.", "description": "Verfeinere Animationen mit <kbd>animation-delay</kbd>, <kbd>animation-iteration-count</kbd>, <kbd>animation-direction</kbd> und <kbd>animation-fill-mode</kbd>.",
"task": "Wende die <kbd>fade</kbd> Animation auf <kbd>.box</kbd> an mit <kbd>animation-name: fade</kbd>, <kbd>animation-duration: 2s</kbd>, <kbd>animation-delay: 1s</kbd>, <kbd>animation-iteration-count: 2</kbd> und <kbd>animation-fill-mode: forwards</kbd>.", "task": "Wende die <kbd>pulse</kbd> Animation auf <kbd>.box</kbd> an mit <kbd>animation-name: pulse</kbd>, <kbd>animation-duration: 2s</kbd>, <kbd>animation-delay: 1s</kbd>, <kbd>animation-iteration-count: 2</kbd> und <kbd>animation-fill-mode: forwards</kbd>.",
"previewHTML": "<div class=\"box\">Fade Demo</div>", "previewHTML": "<div class=\"box\">Pulse</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { width: 100px; height: 100px; background: #4caf50; margin: 2rem auto; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { width: 100px; height: 100px; background: black; color: white; display: flex; align-items: center; justify-content: center; margin: 2rem auto; } @keyframes pulse { 0% { background: black; color: white; transform: scale(1); } 50% { background: white; color: black; transform: scale(1.2); } 100% { background: limegreen; color: black; transform: scale(1); } }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Definiere fade und setze Eigenschaften */\n@keyframes fade { from { opacity: 0; } to { opacity: 1; } }\n.box {", "codePrefix": "/* Apply animation properties */\n.box {",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "}",
"solution": " animation-name: fade;\n animation-duration: 2s;\n animation-delay: 1s;\n animation-iteration-count: 2;\n animation-fill-mode: forwards;", "solution": " animation-name: pulse;\n animation-duration: 2s;\n animation-delay: 1s;\n animation-iteration-count: 2;\n animation-fill-mode: forwards;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "contains", "type": "property_value",
"value": "animation-delay", "value": { "property": "animation-name", "expected": "pulse" },
"message": "Verwende <kbd>animation-delay</kbd>", "message": "Setze <kbd>animation-name: pulse</kbd>"
"options": { "caseSensitive": false }
},
{
"type": "contains",
"value": "animation-iteration-count",
"message": "Verwende <kbd>animation-iteration-count</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "contains",
"value": "animation-fill-mode",
"message": "Verwende <kbd>animation-fill-mode</kbd>",
"options": { "caseSensitive": false }
}, },
{ {
"type": "property_value", "type": "property_value",

View File

@@ -7,13 +7,13 @@
"lessons": [ "lessons": [
{ {
"id": "responsive-1", "id": "responsive-1",
"title": "Einführung in Media Queries", "title": "Media Queries",
"description": "Verstehe die Syntax und Anwendungsfälle für CSS Media Queries, um Stile bedingt basierend auf Viewport-Eigenschaften anzuwenden.", "description": "Verstehe die Syntax und Anwendungsfälle für CSS Media Queries, um Stile bedingt basierend auf Viewport-Eigenschaften anzuwenden.<br><br><pre>@media (max-width: 600px) {\n .panel {\n background: lightcoral;\n }\n}</pre>",
"task": "Schreibe eine Media Query, die gilt, wenn der Viewport maximal 600px breit ist, und ändere den Hintergrund von <kbd>.panel</kbd> auf <kbd>lightcoral</kbd>.", "task": "Schreibe eine Media Query mit <kbd>@media (max-width: 600px)</kbd>, die den Hintergrund von <kbd>.panel</kbd> auf <kbd>lightcoral</kbd> ändert.",
"previewHTML": "<div class=\"panel\">Ändere die Fenstergröße</div>", "previewHTML": "<div class=\"panel\">Resize the window</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .panel { padding: 1rem; background: lightblue; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .panel { padding: 1rem; background: lightblue; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Füge deine Media Query unten ein */\n", "codePrefix": "/* Add your media query below */\n",
"initialCode": "", "initialCode": "",
"codeSuffix": "", "codeSuffix": "",
"solution": "@media (max-width: 600px) {\n .panel {\n background: lightcoral;\n }\n}", "solution": "@media (max-width: 600px) {\n .panel {\n background: lightcoral;\n }\n}",
@@ -22,7 +22,7 @@
{ {
"type": "regex", "type": "regex",
"value": "@media\\s*\\(max-width:\\s*600px\\)", "value": "@media\\s*\\(max-width:\\s*600px\\)",
"message": "Verwende eine Media Query für max-width: 600px", "message": "Verwende <kbd>@media (max-width: 600px)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
@@ -31,68 +31,49 @@
"message": "Adressiere <kbd>.panel</kbd> innerhalb der Media Query", "message": "Adressiere <kbd>.panel</kbd> innerhalb der Media Query",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{
"type": "contains",
"value": "background",
"message": "Ändere die <kbd>background</kbd> Eigenschaft",
"options": { "caseSensitive": false }
},
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "background", "expected": "lightcoral" }, "value": { "property": "background", "expected": "lightcoral" },
"message": "Setze background auf <kbd>lightcoral</kbd>", "message": "Setze <kbd>background: lightcoral</kbd>",
"options": { "exact": false } "options": { "exact": false }
} }
] ]
}, },
{ {
"id": "responsive-2", "id": "responsive-2",
"title": "Flüssige Typografie", "title": "Fluid Type",
"description": "Verwende relative Einheiten wie vw, damit Schriftgrößen mit der Viewport-Breite skalieren.", "description": "Verwende relative Einheiten wie <kbd>vw</kbd>, damit Schriftgrößen mit der Viewport-Breite skalieren.",
"task": "Setze die font-size von <kbd>.text</kbd> auf <kbd>5vw</kbd>, damit sie sich mit dem Viewport ändert.", "task": "Setze <kbd>font-size: 5vw</kbd>, damit sie sich mit dem Viewport ändert.",
"previewHTML": "<p class=\"text\">Flüssige Typografie</p>", "previewHTML": "<p class=\"text\">Fluid Typography</p>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Wende flüssige Schriftgröße an */\n.text {", "codePrefix": "/* Apply fluid font sizing */\n.text {",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "}",
"solution": " font-size: 5vw;", "solution": " font-size: 5vw;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ { "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Setze <kbd>font-size: 5vw</kbd>" }
"type": "contains",
"value": "font-size",
"message": "Verwende die <kbd>font-size</kbd> Eigenschaft",
"options": { "caseSensitive": false }
},
{
"type": "contains",
"value": "vw",
"message": "Verwende <kbd>vw</kbd> Einheit für flüssige Größe",
"options": { "caseSensitive": false }
},
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Setze font-size auf <kbd>5vw</kbd>" }
] ]
}, },
{ {
"id": "responsive-3", "id": "responsive-3",
"title": "Flexible Raster", "title": "Responsive Grid",
"description": "Kombiniere CSS Grid mit auto-fit oder auto-fill für responsive Spaltenlayouts.", "description": "Kombiniere CSS Grid mit <kbd>auto-fit</kbd> oder <kbd>auto-fill</kbd> für responsive Spaltenlayouts, die automatisch die Anzahl der Spalten basierend auf verfügbarem Platz anpassen.",
"task": "Definiere <kbd>.cards</kbd> mit <kbd>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</kbd> und einem gap von <kbd>1rem</kbd>.", "task": "Füge <kbd>display: grid</kbd>, <kbd>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</kbd> und <kbd>gap: 1rem</kbd> hinzu.",
"previewHTML": "<div class=\"cards\"><div>1</div><div>2</div><div>3</div><div>4</div></div>", "previewHTML": "<section class=\"features\"><article class=\"feature\"><h3>Fast</h3><p>Lightning quick load times</p></article><article class=\"feature\"><h3>Secure</h3><p>Enterprise-grade security</p></article><article class=\"feature\"><h3>Reliable</h3><p>99.9% uptime guaranteed</p></article><article class=\"feature\"><h3>Support</h3><p>24/7 customer service</p></article></section>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .cards > div { background: #d1c4e9; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .feature { background: white; padding: 1rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .feature h3 { margin: 0 0 8px; color: steelblue; } .feature p { margin: 0; color: #666; font-size: 0.9rem; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Erstelle ein responsives Raster */\n.cards {", "codePrefix": ".features {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": "display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;", "solution": "display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "contains", "type": "property_value",
"value": "grid-template-columns", "value": { "property": "display", "expected": "grid" },
"message": "Definiere <kbd>grid-template-columns</kbd>", "message": "Setze <kbd>display: grid</kbd>"
"options": { "caseSensitive": false }
}, },
{ {
"type": "regex", "type": "regex",
@@ -100,18 +81,22 @@
"message": "Verwende <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>", "message": "Verwende <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ "type": "contains", "value": "gap", "message": "Verwende die <kbd>gap</kbd> Eigenschaft", "options": { "caseSensitive": false } } {
"type": "property_value",
"value": { "property": "gap", "expected": "1rem" },
"message": "Setze <kbd>gap: 1rem</kbd>"
}
] ]
}, },
{ {
"id": "responsive-4", "id": "responsive-4",
"title": "Mobile-First Media Queries", "title": "Mobile-First",
"description": "Verfolge einen Mobile-First-Ansatz: Schreibe Basis-Stile für kleine Bildschirme und erweitere für größere Viewports.", "description": "Verfolge einen Mobile-First-Ansatz: Schreibe Basis-Stile für kleine Bildschirme und erweitere für größere Viewports.",
"task": "Schreibe eine Media Query für min-width 768px, die die Breite von <kbd>.sidebar</kbd> auf <kbd>250px</kbd> setzt.", "task": "Schreibe eine Media Query mit <kbd>@media (min-width: 768px)</kbd>, die die Breite von <kbd>.sidebar</kbd> auf <kbd>250px</kbd> setzt.",
"previewHTML": "<aside class=\"sidebar\">Seitenleiste</aside>", "previewHTML": "<aside class=\"sidebar\">Sidebar</aside>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Füge Mobile-First-Erweiterung hinzu */\n", "codePrefix": "/* Add mobile-first enhancement */\n",
"initialCode": "", "initialCode": "",
"codeSuffix": "", "codeSuffix": "",
"solution": "@media (min-width: 768px) {\n .sidebar {\n width: 250px;\n }\n}", "solution": "@media (min-width: 768px) {\n .sidebar {\n width: 250px;\n }\n}",
@@ -120,7 +105,7 @@
{ {
"type": "regex", "type": "regex",
"value": "@media\\s*\\(min-width:\\s*768px\\)", "value": "@media\\s*\\(min-width:\\s*768px\\)",
"message": "Verwende eine Media Query für min-width: 768px", "message": "Verwende <kbd>@media (min-width: 768px)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
@@ -132,7 +117,7 @@
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "width", "expected": "250px" }, "value": { "property": "width", "expected": "250px" },
"message": "Setze width auf <kbd>250px</kbd>", "message": "Setze <kbd>width: 250px</kbd>",
"options": { "exact": false } "options": { "exact": false }
} }
] ]

View File

@@ -1,21 +1,21 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-validation", "id": "html-forms-validation",
"title": "HTML Validierung", "title": "Formularvalidierung",
"description": "Lerne die eingebauten HTML5-Formular-Validierungsattribute kennen", "description": "Verwende die eingebaute HTML5-Validierung für bessere Benutzererfahrung",
"mode": "html", "mode": "html",
"difficulty": "intermediate", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "required-fields", "id": "required-fields",
"title": "Pflichtfelder", "title": "Pflichtfelder",
"description": "Das <kbd>required</kbd>-Attribut verhindert das Absenden des Formulars, wenn das Feld leer ist.<br><br>Füge es zu jeder Eingabe hinzu, die ausgefüllt werden muss:<br><kbd>&lt;input type=\"text\" required&gt;</kbd><br><br>Der Browser zeigt automatisch eine Validierungsmeldung an.", "description": "Das <kbd>required</kbd>-Attribut verhindert das Absenden des Formulars, wenn das Feld leer ist. Der Browser zeigt automatisch eine Validierungsmeldung an - kein JavaScript nötig!<br><br>Füge es zu jeder Eingabe hinzu, die ausgefüllt werden muss:<br><kbd>&lt;input type=\"text\" required&gt;</kbd>",
"task": "Mache sowohl das Name- als auch das E-Mail-Feld zu Pflichtfeldern, indem du das <kbd>required</kbd>-Attribut hinzufügst.", "task": "Mache sowohl das Name- als auch das E-Mail-Feld zu Pflichtfeldern, indem du das <kbd>required</kbd>-Attribut zu jeder Eingabe hinzufügst.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-of-type { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid { border-color: #d32f2f; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 320px; } label { display: block; margin-top: 15px; margin-bottom: 5px; font-weight: 500; } label:first-of-type { margin-top: 0; } input { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; } input:focus { outline: 2px solid steelblue; border-color: transparent; } button { margin-top: 20px; padding: 10px 20px; background: steelblue; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 500; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n \n <label for=\"email\">E-Mail: *</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <button type=\"submit\">Absenden</button>\n</form>", "initialCode": "<form>\n <label for=\"name\">Name *</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n \n <label for=\"email\">E-Mail *</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <button type=\"submit\">Absenden</button>\n</form>",
"solution": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n \n <label for=\"email\">E-Mail: *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <button type=\"submit\">Absenden</button>\n</form>", "solution": "<form>\n <label for=\"name\">Name *</label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n \n <label for=\"email\">E-Mail *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <button type=\"submit\">Absenden</button>\n</form>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
@@ -29,84 +29,6 @@
"message": "Füge <kbd>required</kbd> zum E-Mail-Feld hinzu" "message": "Füge <kbd>required</kbd> zum E-Mail-Feld hinzu"
} }
] ]
},
{
"id": "input-constraints",
"title": "Eingabebeschränkungen",
"description": "Kontrolliere, was Benutzer eingeben können:<br><br><kbd>minlength</kbd> / <kbd>maxlength</kbd> - Textlängenbegrenzung<br><kbd>min</kbd> / <kbd>max</kbd> - Zahlenbereich<br><kbd>pattern</kbd> - Regex-Musterabgleich<br><kbd>placeholder</kbd> - Hinweistext (kein Label!)",
"task": "Füge Validierung zur Passwort-Eingabe hinzu:<br>1. Füge <kbd>minlength=\"8\"</kbd> für die Mindestlänge hinzu<br>2. Füge <kbd>maxlength=\"20\"</kbd> für die Maximallänge hinzu<br>3. Füge <kbd>placeholder=\"Passwort eingeben\"</kbd> als Hinweis hinzu",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } label { display: block; margin-bottom: 5px; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid:not(:placeholder-shown) { border-color: #d32f2f; } small { display: block; font-size: 12px; color: #666; margin-top: 4px; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }",
"sandboxCSS": "",
"initialCode": "<form>\n <label for=\"password\">Passwort:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Muss 8-20 Zeichen lang sein</small>\n \n <button type=\"submit\">Konto erstellen</button>\n</form>",
"solution": "<form>\n <label for=\"password\">Passwort:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\" maxlength=\"20\" placeholder=\"Passwort eingeben\" aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Muss 8-20 Zeichen lang sein</small>\n \n <button type=\"submit\">Konto erstellen</button>\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "minlength", "value": "8" },
"message": "Füge <kbd>minlength</kbd>=\"8\" zum Passwort hinzu"
},
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "maxlength", "value": "20" },
"message": "Füge <kbd>maxlength</kbd>=\"20\" zum Passwort hinzu"
},
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "placeholder", "value": null },
"message": "Füge einen <kbd>placeholder</kbd> hinzu, der andeutet, was einzugeben ist"
}
]
},
{
"id": "complete-registration",
"title": "Vollständiges Registrierungsformular",
"description": "Erstelle ein vollständiges Registrierungsformular mit allen Validierungskonzepten:<br><br>- Pflichtfelder mit * markiert<br>- E-Mail-Validierung (type=\"email\" verwenden)<br>- Passwort mit Längenbeschränkungen<br>- AGB-Checkbox (Pflichtfeld)<br>- Absende-Button",
"task": "Vervollständige das Registrierungsformular. Füge <kbd>required</kbd>-Attribute, passende Eingabetypen und Validierungsbeschränkungen hinzu.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 400px; background: #f5f5f5; padding: 25px; border-radius: 8px; } h2 { margin-top: 0; margin-bottom: 20px; } label { display: block; margin-top: 15px; margin-bottom: 5px; font-weight: 500; } input[type='text'], input[type='email'], input[type='password'] { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 14px; } input:focus { outline: 2px solid #1976d2; border-color: transparent; } input[type='checkbox'] { width: auto; margin-right: 8px; vertical-align: middle; } label:has(input[type='checkbox']) { display: flex; align-items: center; font-weight: normal; } button { width: 100%; margin-top: 20px; padding: 12px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 500; } button:hover { background: #1565c0; }",
"sandboxCSS": "",
"initialCode": "<form>\n <h2>Konto erstellen</h2>\n \n <label for=\"fullname\">Vollständiger Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\">\n \n <label for=\"email\">E-Mail *</label>\n <input id=\"email\" name=\"email\">\n \n <label for=\"password\">Passwort *</label>\n <input id=\"password\" name=\"password\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\">\n Ich stimme den Nutzungsbedingungen zu *\n </label>\n \n <button type=\"submit\">Registrieren</button>\n</form>",
"solution": "<form>\n <h2>Konto erstellen</h2>\n \n <label for=\"fullname\">Vollständiger Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\" required>\n \n <label for=\"email\">E-Mail *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <label for=\"password\">Passwort *</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\" required>\n Ich stimme den Nutzungsbedingungen zu *\n </label>\n \n <button type=\"submit\">Registrieren</button>\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "attribute_value",
"value": { "selector": "#fullname", "attr": "required", "value": true },
"message": "Mache das Feld für den vollständigen Namen zum Pflichtfeld"
},
{
"type": "attribute_value",
"value": { "selector": "#email", "attr": "type", "value": "email" },
"message": "Setze den Eingabetyp für E-Mail auf <kbd>email</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#email", "attr": "required", "value": true },
"message": "Mache das E-Mail-Feld zum Pflichtfeld"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "type", "value": "password" },
"message": "Setze den Eingabetyp für Passwort auf <kbd>password</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "required", "value": true },
"message": "Mache das Passwort-Feld zum Pflichtfeld"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "minlength", "value": "8" },
"message": "Füge <kbd>minlength</kbd>=\"8\" zum Passwort hinzu"
},
{
"type": "attribute_value",
"value": { "selector": "#terms", "attr": "required", "value": true },
"message": "Mache die AGB-Checkbox zum Pflichtfeld"
}
]
} }
] ]
} }

View File

@@ -44,12 +44,12 @@
"id": "progress-indeterminate", "id": "progress-indeterminate",
"title": "Unbestimmter Fortschritt", "title": "Unbestimmter Fortschritt",
"description": "Wenn der Fortschritt unbekannt ist (wie beim Laden), lasse das <kbd>value</kbd>-Attribut weg. Dies erstellt einen animierten unbestimmten Zustand.<br><br>Nützlich für Netzwerkanfragen oder Prozesse mit unbekannter Dauer.", "description": "Wenn der Fortschritt unbekannt ist (wie beim Laden), lasse das <kbd>value</kbd>-Attribut weg. Dies erstellt einen animierten unbestimmten Zustand.<br><br>Nützlich für Netzwerkanfragen oder Prozesse mit unbekannter Dauer.",
"task": "Erstelle eine Ladeanzeige:<br>1. Füge ein <kbd>&lt;p&gt;</kbd> mit <code>Lädt...</code> hinzu<br>2. Füge ein <kbd>&lt;progress&gt;</kbd> ohne value-Attribut hinzu", "task": "Erstelle eine Ladeanzeige:<br>1. Füge ein <kbd>&lt;p&gt;</kbd> mit <code>Loading...</code> hinzu<br>2. Füge ein <kbd>&lt;progress&gt;</kbd> ohne value-Attribut hinzu",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } p { margin-bottom: 10px; color: #666; } progress { width: 100%; height: 8px; border-radius: 4px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 4px; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } p { margin-bottom: 10px; color: #666; } progress { width: 100%; height: 8px; border-radius: 4px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 4px; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<p>Lädt...</p>\n<progress></progress>", "solution": "<p>Loading...</p>\n<progress></progress>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
@@ -68,12 +68,12 @@
"id": "meter-gauge", "id": "meter-gauge",
"title": "Meter-Anzeigen", "title": "Meter-Anzeigen",
"description": "Das <kbd>&lt;meter&gt;</kbd>-Element zeigt einen Skalarwert innerhalb eines Bereichs. Verwende es für Messungen wie Speicherplatz, Akku oder Bewertungen.<br><br>Setze <kbd>low</kbd>, <kbd>high</kbd> und <kbd>optimum</kbd>, um gute/schlechte Bereiche zu definieren - der Browser färbt es entsprechend ein!", "description": "Das <kbd>&lt;meter&gt;</kbd>-Element zeigt einen Skalarwert innerhalb eines Bereichs. Verwende es für Messungen wie Speicherplatz, Akku oder Bewertungen.<br><br>Setze <kbd>low</kbd>, <kbd>high</kbd> und <kbd>optimum</kbd>, um gute/schlechte Bereiche zu definieren - der Browser färbt es entsprechend ein!",
"task": "Erstelle eine Akku-Anzeige:<br>1. Füge ein <kbd>&lt;label&gt;</kbd> mit <code>Akku:</code> hinzu<br>2. Füge ein <kbd>&lt;meter&gt;</kbd> hinzu mit:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> und <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> und <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>", "task": "Erstelle eine Akku-Anzeige:<br>1. Füge ein <kbd>&lt;label&gt;</kbd> mit <code>Battery:</code> hinzu<br>2. Füge ein <kbd>&lt;meter&gt;</kbd> hinzu mit:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> und <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> und <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } meter { width: 100%; height: 25px; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } meter { width: 100%; height: 25px; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<label for=\"battery\">Akku:</label>\n<meter id=\"battery\" value=\"0.8\" min=\"0\" max=\"1\" low=\"0.2\" high=\"0.8\" optimum=\"1\">80%</meter>", "solution": "<label for=\"battery\">Battery:</label>\n<meter id=\"battery\" value=\"0.8\" min=\"0\" max=\"1\" low=\"0.2\" high=\"0.8\" optimum=\"1\">80%</meter>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
@@ -86,11 +86,31 @@
"value": { "selector": "meter", "attr": "value", "value": "0.8" }, "value": { "selector": "meter", "attr": "value", "value": "0.8" },
"message": "Setze <kbd>value=</kbd>\"0.8\" beim Meter" "message": "Setze <kbd>value=</kbd>\"0.8\" beim Meter"
}, },
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "min", "value": "0" },
"message": "Setze <kbd>min=</kbd>\"0\" beim Meter"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "max", "value": "1" },
"message": "Setze <kbd>max=</kbd>\"1\" beim Meter"
},
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "meter", "attr": "low", "value": "0.2" }, "value": { "selector": "meter", "attr": "low", "value": "0.2" },
"message": "Setze <kbd>low=</kbd>\"0.2\", um den niedrigen Schwellenwert zu definieren" "message": "Setze <kbd>low=</kbd>\"0.2\", um den niedrigen Schwellenwert zu definieren"
}, },
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "high", "value": "0.8" },
"message": "Setze <kbd>high=</kbd>\"0.8\", um den hohen Schwellenwert zu definieren"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "optimum", "value": "1" },
"message": "Setze <kbd>optimum=</kbd>\"1\", um den optimalen Wert anzugeben"
},
{ {
"type": "element_exists", "type": "element_exists",
"value": "label", "value": "label",

View File

@@ -2,20 +2,20 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-tables", "id": "html-tables",
"title": "HTML Tabellen", "title": "HTML Tabellen",
"description": "Erstelle strukturierte Datentabellen mit Überschriften und Beschriftungen", "description": "Erstelle strukturierte Datentabellen mit semantischem Markup",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "table-basic", "id": "table-basic",
"title": "Grundlegende Tabellenstruktur", "title": "Datentabellen",
"description": "Tabellen verwenden <kbd>&lt;table&gt;</kbd> mit <kbd>&lt;tr&gt;</kbd> für Zeilen. In Zeilen nutze <kbd>&lt;th&gt;</kbd> für Überschriften und <kbd>&lt;td&gt;</kbd> für Datenzellen.<br><br>Das <kbd>&lt;caption&gt;</kbd>-Element bietet einen zugänglichen Titelr die Tabelle.", "description": "Tabellen zeigen strukturierte Daten in Zeilen und Spalten. Verwende <kbd>&lt;table&gt;</kbd> als Container, <kbd>&lt;tr&gt;</kbd> für Zeilen, <kbd>&lt;th&gt;</kbd> für Kopfzellen und <kbd>&lt;td&gt;</kbd> für Datenzellen.<br><br>Füge <kbd>&lt;caption&gt;</kbd> hinzu für einen zugänglichen Titel, der den Tabelleninhalt beschreibt.",
"task": "Erstelle eine einfache Tabelle mit:<br>1. Einer <kbd>&lt;caption&gt;</kbd> mit <code>Obstpreise</code><br>2. Einer Kopfzeile mit <code>Obst</code> und <code>Preis</code> Spalten<br>3. Mindestens 2 Datenzeilen", "task": "Erstelle eine Preistabelle:<br>1. Eine <kbd>&lt;caption&gt;</kbd> mit <code>Pricing</code><br>2. Eine Kopfzeile mit <code>Plan</code> und <code>Price</code><br>3. Zwei Datenzeilen für Basic ($9) und Pro ($29)",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px; font-weight: 600; font-size: 1.2rem; color: #333; background: #f8f9fa; } th, td { padding: 12px 20px; text-align: left; border-bottom: 1px solid #eee; } th { background: #3498db; color: white; font-weight: 500; } tr:hover { background: #f8f9fa; } tr:last-child td { border-bottom: none; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; } table { border-collapse: collapse; width: 100%; max-width: 350px; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 16px; font-weight: 600; font-size: 1.1rem; color: #333; background: #f8f9fa; } th, td { padding: 14px 20px; text-align: left; border-bottom: 1px solid #eee; } th { background: steelblue; color: white; font-weight: 500; } tr:last-child td { border-bottom: none; } tr:hover td { background: #f8f9fa; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<table>\n <caption>Obstpreise</caption>\n <tr>\n <th>Obst</th>\n <th>Preis</th>\n </tr>\n <tr>\n <td>Apfel</td>\n <td>1,50 €</td>\n </tr>\n <tr>\n <td>Banane</td>\n <td>0,75 €</td>\n </tr>\n</table>", "solution": "<table>\n <caption>Pricing</caption>\n <tr>\n <th>Plan</th>\n <th>Price</th>\n </tr>\n <tr>\n <td>Basic</td>\n <td>$9</td>\n </tr>\n <tr>\n <td>Pro</td>\n <td>$29</td>\n </tr>\n</table>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
@@ -26,100 +26,17 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "caption", "value": "caption",
"message": "Füge eine <kbd>&lt;caption&gt;</kbd> als Tabellentitel hinzu" "message": "Füge eine <kbd>&lt;caption&gt;</kbd> für den Tabellentitel hinzu"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "th", "min": 2 }, "value": { "selector": "th", "min": 2 },
"message": "Füge mindestens 2 Überschriftszellen (th) hinzu" "message": "Füge Kopfzellen (<kbd>&lt;th&gt;</kbd>) für Plan und Price hinzu"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "tr", "min": 3 }, "value": { "selector": "tr", "min": 3 },
"message": "Füge mindestens 3 Zeilen hinzu (1 Kopf + 2 Daten)" "message": "Füge 3 Zeilen hinzu (1 Kopf + 2 Datenzeilen)"
}
]
},
{
"id": "table-thead-tbody",
"title": "Tabellenkopf & -körper",
"description": "Verwende <kbd>&lt;thead&gt;</kbd> zum Gruppieren von Kopfzeilen und <kbd>&lt;tbody&gt;</kbd> zum Gruppieren von Datenzeilen. Das hilft Browsern und Hilfstechnologien, die Tabellenstruktur zu verstehen.<br><br>Du kannst auch <kbd>&lt;tfoot&gt;</kbd> für Fußzeilen wie Summen verwenden.",
"task": "Erstelle eine strukturierte Tabelle:<br>1. Eine <kbd>&lt;caption&gt;</kbd> mit <code>Monatliche Verkäufe</code><br>2. Ein <kbd>&lt;thead&gt;</kbd> mit Monat und Umsatz Überschriften<br>3. Ein <kbd>&lt;tbody&gt;</kbd> mit mindestens 2 Datenzeilen",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } table { border-collapse: collapse; width: 100%; max-width: 450px; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.2); } caption { padding: 20px; font-weight: 700; font-size: 1.3rem; color: white; background: transparent; text-shadow: 0 2px 4px rgba(0,0,0,0.2); caption-side: top; } thead { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } th { padding: 15px 20px; text-align: left; color: white; font-weight: 500; } tbody tr { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } td { padding: 15px 20px; color: #333; } tbody tr:last-child { border-bottom: none; }",
"sandboxCSS": "",
"initialCode": "",
"solution": "<table>\n <caption>Monatliche Verkäufe</caption>\n <thead>\n <tr>\n <th>Monat</th>\n <th>Umsatz</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>Januar</td>\n <td>12.500 €</td>\n </tr>\n <tr>\n <td>Februar</td>\n <td>14.200 €</td>\n </tr>\n </tbody>\n</table>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "table",
"message": "Füge ein <kbd>&lt;table&gt;</kbd>-Element hinzu"
},
{
"type": "element_exists",
"value": "caption",
"message": "Füge ein <kbd>&lt;caption&gt;</kbd>-Element hinzu"
},
{
"type": "element_exists",
"value": "thead",
"message": "Füge ein <kbd>&lt;thead&gt;</kbd> für den Kopfbereich hinzu"
},
{
"type": "element_exists",
"value": "tbody",
"message": "Füge ein <kbd>&lt;tbody&gt;</kbd> für die Datenzeilen hinzu"
},
{
"type": "element_count",
"value": { "selector": "tbody tr", "min": 2 },
"message": "Füge mindestens 2 Datenzeilen in tbody hinzu"
}
]
},
{
"id": "table-complete",
"title": "Vollständige Tabelle mit Fuß",
"description": "Füge <kbd>&lt;tfoot&gt;</kbd> hinzu, um einen Fußbereich für Summen oder Zusammenfassungen zu erstellen. Der Fuß bleibt unten, auch wenn tbody viele Zeilen hat.<br><br>Kombiniere alle Abschnitte für eine vollständig strukturierte, zugängliche Tabelle.",
"task": "Erstelle eine vollständige Tabelle:<br>1. Eine <kbd>&lt;caption&gt;</kbd> mit <code>Bestellübersicht</code><br>2. Ein <kbd>&lt;thead&gt;</kbd> mit Artikel und Preis Überschriften<br>3. Ein <kbd>&lt;tbody&gt;</kbd> mit 2 Artikeln<br>4. Ein <kbd>&lt;tfoot&gt;</kbd> mit einer Summenzeile",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #fafafa; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px 20px; font-weight: 600; font-size: 1.1rem; color: #333; text-align: left; background: #f8f9fa; border-bottom: 2px solid #eee; } th, td { padding: 12px 20px; text-align: left; } thead th { background: #2c3e50; color: white; } tbody td { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } tfoot { background: #ecf0f1; font-weight: 600; } tfoot td { border-top: 2px solid #2c3e50; color: #2c3e50; }",
"sandboxCSS": "",
"initialCode": "",
"solution": "<table>\n <caption>Bestellübersicht</caption>\n <thead>\n <tr>\n <th>Artikel</th>\n <th>Preis</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>Widget</td>\n <td>25,00 €</td>\n </tr>\n <tr>\n <td>Gadget</td>\n <td>35,00 €</td>\n </tr>\n </tbody>\n <tfoot>\n <tr>\n <td>Gesamt</td>\n <td>60,00 €</td>\n </tr>\n </tfoot>\n</table>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "table",
"message": "Füge ein <kbd>&lt;table&gt;</kbd>-Element hinzu"
},
{
"type": "element_exists",
"value": "caption",
"message": "Füge ein <kbd>&lt;caption&gt;</kbd>-Element hinzu"
},
{
"type": "element_exists",
"value": "thead",
"message": "Füge einen <kbd>&lt;thead&gt;</kbd>-Abschnitt hinzu"
},
{
"type": "element_exists",
"value": "tbody",
"message": "Füge einen <kbd>&lt;tbody&gt;</kbd>-Abschnitt hinzu"
},
{
"type": "element_exists",
"value": "tfoot",
"message": "Füge einen <kbd>&lt;tfoot&gt;</kbd>-Abschnitt für die Summe hinzu"
},
{
"type": "element_count",
"value": { "selector": "tbody tr", "min": 2 },
"message": "Füge mindestens 2 Artikelzeilen in tbody hinzu"
} }
] ]
} }

View File

@@ -15,7 +15,7 @@
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; display: flex; justify-content: center; } svg { background: white; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; display: flex; justify-content: center; } svg { background: white; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"#3498db\" />\n</svg>", "solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"steelblue\" />\n</svg>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
@@ -28,6 +28,16 @@
"value": "circle", "value": "circle",
"message": "Füge ein <kbd>&lt;circle&gt;</kbd>-Element in das SVG ein" "message": "Füge ein <kbd>&lt;circle&gt;</kbd>-Element in das SVG ein"
}, },
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "width", "value": "200" },
"message": "Setze <kbd>width=</kbd>\"200\" beim SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "height", "value": "200" },
"message": "Setze <kbd>height=</kbd>\"200\" beim SVG"
},
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "circle", "attr": "cx", "value": "100" }, "value": { "selector": "circle", "attr": "cx", "value": "100" },
@@ -37,6 +47,11 @@
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "circle", "attr": "cy", "value": "100" }, "value": { "selector": "circle", "attr": "cy", "value": "100" },
"message": "Setze <kbd>cy=</kbd>\"100\" für das vertikale Zentrum" "message": "Setze <kbd>cy=</kbd>\"100\" für das vertikale Zentrum"
},
{
"type": "attribute_value",
"value": { "selector": "circle", "attr": "r", "value": "50" },
"message": "Setze <kbd>r=</kbd>\"50\" für den Radius"
} }
] ]
}, },
@@ -49,7 +64,7 @@
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 200px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 200px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<svg width=\"200\" height=\"150\">\n <rect x=\"20\" y=\"20\" width=\"80\" height=\"60\" fill=\"#e74c3c\" />\n <line x1=\"120\" y1=\"30\" x2=\"180\" y2=\"90\" stroke=\"#2c3e50\" stroke-width=\"3\" />\n</svg>", "solution": "<svg width=\"200\" height=\"150\">\n <rect x=\"20\" y=\"20\" width=\"80\" height=\"60\" fill=\"tomato\" />\n <line x1=\"120\" y1=\"30\" x2=\"180\" y2=\"90\" stroke=\"slategray\" stroke-width=\"3\" />\n</svg>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
@@ -66,6 +81,61 @@
"type": "element_exists", "type": "element_exists",
"value": "line", "value": "line",
"message": "Füge ein <kbd>&lt;line&gt;</kbd>-Element hinzu" "message": "Füge ein <kbd>&lt;line&gt;</kbd>-Element hinzu"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "width", "value": "200" },
"message": "Setze <kbd>width=</kbd>\"200\" beim SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "height", "value": "150" },
"message": "Setze <kbd>height=</kbd>\"150\" beim SVG"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "x", "value": "20" },
"message": "Setze <kbd>x=</kbd>\"20\" beim rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "y", "value": "20" },
"message": "Setze <kbd>y=</kbd>\"20\" beim rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "width", "value": "80" },
"message": "Setze <kbd>width=</kbd>\"80\" beim rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "height", "value": "60" },
"message": "Setze <kbd>height=</kbd>\"60\" beim rect"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "x1", "value": "120" },
"message": "Setze <kbd>x1=</kbd>\"120\" bei der line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "y1", "value": "30" },
"message": "Setze <kbd>y1=</kbd>\"30\" bei der line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "x2", "value": "180" },
"message": "Setze <kbd>x2=</kbd>\"180\" bei der line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "y2", "value": "90" },
"message": "Setze <kbd>y2=</kbd>\"90\" bei der line"
},
{
"type": "contains",
"value": "stroke",
"message": "Füge eine <kbd>stroke</kbd>-Farbe zur line hinzu"
} }
] ]
}, },
@@ -78,7 +148,7 @@
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); min-height: 250px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); min-height: 250px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"80\" fill=\"#f1c40f\" stroke=\"#e67e22\" stroke-width=\"4\" />\n <circle cx=\"70\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <circle cx=\"130\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <line x1=\"60\" y1=\"130\" x2=\"140\" y2=\"130\" stroke=\"#2c3e50\" stroke-width=\"4\" stroke-linecap=\"round\" />\n</svg>", "solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"80\" fill=\"gold\" stroke=\"orange\" stroke-width=\"4\" />\n <circle cx=\"70\" cy=\"80\" r=\"10\" fill=\"darkslategray\" />\n <circle cx=\"130\" cy=\"80\" r=\"10\" fill=\"darkslategray\" />\n <line x1=\"60\" y1=\"130\" x2=\"140\" y2=\"130\" stroke=\"darkslategray\" stroke-width=\"4\" stroke-linecap=\"round\" />\n</svg>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "welcome", "id": "welcome",
"title": "Code Crispies", "title": "Code Crispies",
"description": "Welcome to Code Crispies - your interactive web development learning platform", "description": "Bienvenido a Code Crispies - tu plataforma interactiva de aprendizaje de desarrollo web",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "get-started", "id": "get-started",
"title": "Get Started", "title": "Comenzar",
"description": "<strong>Code Crispies</strong> is a free, open-source platform for learning web development through hands-on exercises. No account required!<br><br><strong>What you'll learn:</strong><br>• <strong>HTML</strong> - Semantic elements, forms, tables, SVG (<em>HTML Block & Inline</em>, <em>HTML Forms</em>, <em>HTML Tables</em>)<br>• <strong>CSS</strong> - Selectors, box model, flexbox, animations (<em>CSS Selectors</em>, <em>CSS Box Model</em>, <em>CSS Flexbox</em>)<br>• <strong>Responsive Design</strong> - Media queries and mobile-first layouts<br><br><strong>How it works:</strong><br>1. Read the task in the left panel<br>2. Write code in the editor<br>3. See live results in the preview<br>4. Get instant feedback with hints<br><br><strong>Keyboard shortcuts:</strong> <kbd>Ctrl+Z</kbd> to undo, <kbd>Ctrl+Shift+Z</kbd> to redo<br><br><strong>More resources:</strong><br>• <a href=\"https://nextlevelshit.github.io/html-over-js/\" target=\"_blank\">HTML over JS</a> - Native HTML vs JavaScript solutions<br>• <a href=\"https://nextlevelshit.github.io/web-engineering-mandala/\" target=\"_blank\">Web Engineering Mandala</a> - JavaScript technology roadmap", "description": "<strong>Code Crispies</strong> es una plataforma gratuita y de código abierto para aprender desarrollo web mediante ejercicios prácticos. ¡No se requiere cuenta!<br><br><strong>Lo que aprenderás:</strong><br>• <strong>HTML</strong> - Elementos semánticos, formularios, tablas, SVG (<em>HTML Bloque e Inline</em>, <em>HTML Formularios</em>, <em>HTML Tablas</em>)<br>• <strong>CSS</strong> - Selectores, modelo de caja, flexbox, animaciones (<em>CSS Selectores</em>, <em>CSS Modelo de Caja</em>, <em>CSS Flexbox</em>)<br>• <strong>Diseño Responsivo</strong> - Media queries y layouts mobile-first<br><br><strong>Cómo funciona:</strong><br>1. Lee la tarea en el panel izquierdo<br>2. Escribe código en el editor<br>3. Ve los resultados en vivo en la vista previa<br>4. Recibe retroalimentación instantánea con pistas<br><br><strong>Atajos de teclado:</strong> <kbd>Ctrl+Z</kbd> deshacer, <kbd>Ctrl+Shift+Z</kbd> rehacer<br><br><strong>Más recursos:</strong><br>• <a href=\"https://nextlevelshit.github.io/html-over-js/\" target=\"_blank\">HTML over JS</a> - HTML nativo vs soluciones JavaScript<br>• <a href=\"https://nextlevelshit.github.io/web-engineering-mandala/\" target=\"_blank\">Web Engineering Mandala</a> - Mapa de tecnologías JavaScript",
"task": "Write <code>Hello World</code>", "task": "Escribe <code>Hello World</code>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; font-size: 2rem; color: #6366f1; font-weight: bold; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; font-size: 2rem; color: #6366f1; font-weight: bold; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,15 +21,15 @@
{ {
"type": "contains", "type": "contains",
"value": "Hello World", "value": "Hello World",
"message": "Write <code>Hello World</code>" "message": "Escribe <code>Hello World</code>"
} }
] ]
}, },
{ {
"id": "overview", "id": "overview",
"title": "Overview", "title": "Vista General",
"description": "<strong>You're ready!</strong> Open the menu (☰) to explore all modules.<br><br><strong>Recommended learning path:</strong><br>1. <em>HTML Block & Inline</em> - Understand container vs inline elements<br>2. <em>HTML Forms</em> - Build interactive forms with validation<br>3. <em>CSS Selectors</em> - Target elements precisely<br>4. <em>CSS Box Model</em> - Master padding, margin, borders<br>5. <em>CSS Flexbox</em> - Create flexible layouts<br>6. <em>CSS Animations</em> - Add motion and transitions<br><br><strong>Tips:</strong><br>• Use <em>Show Expected</em> to see the target result<br>• Your progress is saved automatically<br>• Try Emmet in HTML mode: <kbd>ul>li*3</kbd> + Tab<br><br><strong>Open Source:</strong><br>• <a href=\"https://git.librete.ch/public/code-crispies\" target=\"_blank\">Gitea (Source)</a> · <a href=\"https://github.com/nextlevelshit/code-crispies\" target=\"_blank\">GitHub (Mirror)</a><br>• Made by <a href=\"https://librete.ch\" target=\"_blank\">LibreTECH</a> · <a href=\"https://www.linkedin.com/in/michael-werner-czechowski\" target=\"_blank\">Michael Czechowski</a>", "description": "<strong>¡Estás listo!</strong> Abre el menú (☰) para explorar todos los módulos.<br><br><strong>Ruta de aprendizaje recomendada:</strong><br>1. <em>HTML Bloque e Inline</em> - Entiende elementos contenedores vs inline<br>2. <em>HTML Formularios</em> - Crea formularios interactivos con validación<br>3. <em>CSS Selectores</em> - Selecciona elementos con precisión<br>4. <em>CSS Modelo de Caja</em> - Domina padding, margin, borders<br>5. <em>CSS Flexbox</em> - Crea layouts flexibles<br>6. <em>CSS Animaciones</em> - Añade movimiento y transiciones<br><br><strong>Consejos:</strong><br>• Usa <em>Mostrar Esperado</em> para ver el resultado objetivo<br>• Tu progreso se guarda automáticamente<br>• Prueba Emmet en modo HTML: <kbd>ul>li*3</kbd> + Tab<br><br><strong>Open Source:</strong><br>• <a href=\"https://git.librete.ch/public/code-crispies\" target=\"_blank\">Gitea (Source)</a> · <a href=\"https://github.com/nextlevelshit/code-crispies\" target=\"_blank\">GitHub (Mirror)</a><br>• Hecho por <a href=\"https://librete.ch\" target=\"_blank\">LibreTECH</a> · <a href=\"https://www.linkedin.com/in/michael-werner-czechowski\" target=\"_blank\">Michael Czechowski</a>",
"task": "Click Next to continue", "task": "Haz clic en Siguiente para continuar",
"previewHTML": "<p>Hello World! 🌍</p><p>Hallo Welt!</p><p>Bonjour le monde!</p><p>¡Hola Mundo!</p><p>Ciao Mondo!</p><p>Olá Mundo!</p><p>こんにちは世界!</p><p>你好世界!</p><p>안녕 세상!</p><p>Привет мир!</p><p dir=\"rtl\">שלום עולם!</p><p dir=\"rtl\">مرحبا بالعالم!</p>", "previewHTML": "<p>Hello World! 🌍</p><p>Hallo Welt!</p><p>Bonjour le monde!</p><p>¡Hola Mundo!</p><p>Ciao Mondo!</p><p>Olá Mundo!</p><p>こんにちは世界!</p><p>你好世界!</p><p>안녕 세상!</p><p>Привет мир!</p><p dir=\"rtl\">שלום עולם!</p><p dir=\"rtl\">مرحبا بالعالم!</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 12px; } p { margin: 6px 0; padding: 6px 12px; border-radius: 6px; font-weight: 600; font-size: 0.95em; } p:nth-child(1) { background: #fef3c7; color: #92400e; font-size: 1.2em; } p:nth-child(2) { background: #dcfce7; color: #166534; } p:nth-child(3) { background: #dbeafe; color: #1e40af; } p:nth-child(4) { background: #fce7f3; color: #9d174d; } p:nth-child(5) { background: #e0e7ff; color: #4338ca; } p:nth-child(6) { background: #fef9c3; color: #854d0e; } p:nth-child(7) { background: #fee2e2; color: #991b1b; } p:nth-child(8) { background: #f3e8ff; color: #7c3aed; } p:nth-child(9) { background: #ccfbf1; color: #0f766e; } p:nth-child(10) { background: #fae8ff; color: #86198f; } p:nth-child(11) { background: #fef3c7; color: #b45309; } p:nth-child(12) { background: #d1fae5; color: #047857; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 12px; } p { margin: 6px 0; padding: 6px 12px; border-radius: 6px; font-weight: 600; font-size: 0.95em; } p:nth-child(1) { background: #fef3c7; color: #92400e; font-size: 1.2em; } p:nth-child(2) { background: #dcfce7; color: #166534; } p:nth-child(3) { background: #dbeafe; color: #1e40af; } p:nth-child(4) { background: #fce7f3; color: #9d174d; } p:nth-child(5) { background: #e0e7ff; color: #4338ca; } p:nth-child(6) { background: #fef9c3; color: #854d0e; } p:nth-child(7) { background: #fee2e2; color: #991b1b; } p:nth-child(8) { background: #f3e8ff; color: #7c3aed; } p:nth-child(9) { background: #ccfbf1; color: #0f766e; } p:nth-child(10) { background: #fae8ff; color: #86198f; } p:nth-child(11) { background: #fef3c7; color: #b45309; } p:nth-child(12) { background: #d1fae5; color: #047857; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -39,8 +39,8 @@
"validations": [ "validations": [
{ {
"type": "contains", "type": "contains",
"value": "Hello World", "value": "Hello",
"message": "Click Next to continue" "message": "Haz clic en Siguiente para continuar"
} }
] ]
}, },

View File

@@ -2,18 +2,18 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "box-model", "id": "box-model",
"title": "CSS Box Model", "title": "CSS Box Model",
"description": "Master the fundamental principles of space management in web design through the CSS box model. This module explores how content, padding, borders, and margins combine to create layout structures that are both visually appealing and structurally sound.", "description": "Domina los principios fundamentales de gestión del espacio en diseño web a través del modelo de caja CSS. Este módulo explora cómo el contenido, padding, bordes y márgenes se combinan para crear estructuras de diseño.",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "box-model-1", "id": "box-model-1",
"title": "Box Model Components", "title": "Padding",
"description": "The CSS box model consists of four concentric layers: content area (innermost), padding, border, and margin (outermost). Understanding how these components interact is essential for precise layout control.", "description": "Cada elemento en CSS es una caja con cuatro capas: contenido, padding, borde y margen. <strong>Padding</strong> crea espacio entre tu contenido y el borde de la caja.<br><br>Sin padding, el texto se aprieta incómodamente contra los bordes. El padding hace que el contenido sea legible y visualmente equilibrado.<br><br><pre>.card {\n padding: 1rem;\n}</pre>",
"task": "Set <kbd>padding</kbd> to <kbd>1rem</kbd> to create space between the content and border.", "task": "Esta tarjeta de perfil se ve apretada. Añade <kbd>padding: 1rem</kbd> para que el texto tenga espacio para respirar.",
"previewHTML": "<div class=\"box\">Box Model Components</div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: lavender; border: 2px dashed slategray; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".box {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "padding: 1rem;", "solution": "padding: 1rem;",
@@ -22,62 +22,62 @@
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "padding", "expected": "1rem" }, "value": { "property": "padding", "expected": "1rem" },
"message": "Set <kbd>padding: 1rem</kbd>" "message": "Establece <kbd>padding: 1rem</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-2", "id": "box-model-2",
"title": "Adding Borders", "title": "Borders",
"description": "Borders outline an element, creating visual separation from surrounding content. The border shorthand accepts three values: width, style, and color.", "description": "Los bordes crean límites visuales alrededor de los elementos. El atajo <kbd>border</kbd> acepta tres valores: ancho, estilo y color.<br><br>Estilos comunes: <kbd>solid</kbd>, <kbd>dashed</kbd>, <kbd>dotted</kbd>, <kbd>none</kbd>",
"task": "Set <kbd>border</kbd> to <kbd>2px solid darkslategray</kbd>.", "task": "Añade un acento sutil a la izquierda de la tarjeta con <kbd>border-left: 4px solid steelblue</kbd>.",
"previewHTML": "<div class=\"box\">This box needs a border</div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: mintcream; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".box {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "border: 2px solid darkslategray;", "solution": "border-left: 4px solid steelblue;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "regex", "type": "regex",
"value": "border:\\s*2px\\s+solid\\s+darkslategray", "value": "border-left:\\s*4px\\s+solid\\s+steelblue",
"message": "Set <kbd>border: 2px solid darkslategray</kbd>", "message": "Establece <kbd>border-left: 4px solid steelblue</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "box-model-3", "id": "box-model-3",
"title": "Adding Margins", "title": "Margins",
"description": "Margins create space between elements, controlling how they relate to one another within a layout. Unlike padding (which affects internal spacing), margins exist outside the element's border.", "description": "Los márgenes crean espacio <em>fuera</em> del elemento, separándolo de sus vecinos. Mientras que el padding empuja el contenido hacia adentro, los márgenes empujan otros elementos hacia afuera.",
"task": "Set <kbd>margin</kbd> to <kbd>1rem</kbd> to create space between this element and its neighbors.", "task": "Añade espacio entre estas dos tarjetas de perfil con <kbd>margin-bottom: 1rem</kbd> en <kbd>.card</kbd>.",
"previewHTML": "<div class=\"container\"><div class=\"outer\">This box needs margins</div><div class=\"neighbor\">Adjacent element</div></div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article><article class=\"card\"><h3>Alex Rivera</h3><p>UX Designer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .outer { background-color: plum; padding: 1rem; border: 2px solid orchid; } .neighbor { background-color: lightblue; padding: 1rem; border: 2px solid steelblue; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".outer {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "margin: 1rem;", "solution": "margin-bottom: 1rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "margin", "expected": "1rem" }, "value": { "property": "margin-bottom", "expected": "1rem" },
"message": "Set <kbd>margin: 1rem</kbd>" "message": "Establece <kbd>margin-bottom: 1rem</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-4", "id": "box-model-4",
"title": "Box Sizing: Border-Box", "title": "Box Sizing",
"description": "The <kbd>box-sizing</kbd> property determines how element dimensions are calculated. The default <kbd>content-box</kbd> excludes padding and border from width/height, while <kbd>border-box</kbd> includes them, making layout calculations more intuitive.", "description": "Por defecto, <kbd>width</kbd> solo establece el ancho del contenido. Padding y bordes se suman al total. Esto causa problemas de diseño.<br><br><kbd>box-sizing: border-box</kbd> incluye padding y borde en el ancho, haciendo el dimensionamiento predecible. La mayoría de desarrolladores aplican esto a todos los elementos.",
"task": "Set <kbd>box-sizing</kbd> to <kbd>border-box</kbd> so padding and border are included in the width.", "task": "Ambas tarjetas tienen <kbd>width: 200px</kbd>. La izquierda usa el tamaño predeterminado (content-box), haciéndola más ancha de lo esperado. Corrige la tarjeta derecha con <kbd>box-sizing: border-box</kbd>.",
"previewHTML": "<div class=\"sizing-demo\"><div class=\"box default\">Content-box (default)</div><div class=\"box sized\">Border-box</div></div>", "previewHTML": "<div class=\"demo\"><article class=\"card\">Content-box</article><article class=\"card fix\">Border-box</article></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .sizing-demo { display: flex; gap: 1rem; } .box { width: 200px; padding: 1rem; border: 4px solid teal; background: lightcyan; } .default { box-sizing: content-box; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .demo { display: flex; gap: 1rem; } .card { width: 200px; padding: 1rem; border: 4px solid steelblue; background: white; border-radius: 8px; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".sized {\n ", "codePrefix": ".fix {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "box-sizing: border-box;", "solution": "box-sizing: border-box;",
@@ -86,93 +86,104 @@
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "box-sizing", "expected": "border-box" }, "value": { "property": "box-sizing", "expected": "border-box" },
"message": "Set <kbd>box-sizing: border-box</kbd>" "message": "Establece <kbd>box-sizing: border-box</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-5", "id": "box-model-5",
"title": "Margin Collapse", "title": "Padding Shorthand",
"description": "When two vertical margins meet, they collapse to the larger value instead of adding up. Understanding this behavior is crucial for consistent vertical spacing.", "description": "Padding acepta 1-4 valores:<br>• 1 valor: todos los lados<br>• 2 valores: vertical | horizontal<br>• 4 valores: arriba | derecha | abajo | izquierda",
"task": "Set <kbd>margin-bottom</kbd> to <kbd>2rem</kbd>. Notice the space between paragraphs equals 2rem (not 3rem) due to margin collapse.", "task": "Este botón necesita más espacio horizontal que vertical. Establece <kbd>padding: 8px 1rem</kbd> (8px arriba/abajo, 1rem izquierda/derecha).",
"previewHTML": "<div class=\"collapse-demo\"><p class=\"first\">This paragraph has a bottom margin.</p><p class=\"second\">This paragraph has a top margin of 1rem.</p></div>", "previewHTML": "<button class=\"btn\">Follow</button>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .collapse-demo { border: 1px solid silver; padding: 8px; background: ghostwhite; } .second { margin-top: 1rem; background: linen; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".first {\n ", "codePrefix": ".btn {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "margin-bottom: 2rem;", "solution": "padding: 8px 1rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "regex",
"value": { "property": "margin-bottom", "expected": "2rem" }, "value": "padding:\\s*8px\\s+1rem",
"message": "Set <kbd>margin-bottom: 2rem</kbd>" "message": "Establece <kbd>padding: 8px 1rem</kbd>",
"options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "box-model-6", "id": "box-model-6",
"title": "Margin Shorthand Notation", "title": "Margin Shorthand",
"description": "The margin shorthand can set all four sides at once. Two values set vertical (top/bottom) and horizontal (left/right) margins respectively.", "description": "Margin usa el mismo patrón de atajo que padding. Un patrón común es centrar elementos de bloque horizontalmente con <kbd>margin: 0 auto</kbd>.",
"task": "Set <kbd>margin</kbd> to <kbd>1rem 2rem</kbd> for 1rem top/bottom and 2rem left/right.", "task": "Centra esta tarjeta horizontalmente. Establece <kbd>margin: 0 auto</kbd> para calcular automáticamente márgenes iguales izquierda/derecha.",
"previewHTML": "<div class=\"container\"><div class=\"spaced\">This box needs margins: 1rem top/bottom, 2rem left/right</div></div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .spaced { background-color: honeydew; border: 2px solid mediumseagreen; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { width: 250px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".spaced {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "margin: 1rem 2rem;", "solution": "margin: 0 auto;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "regex", "type": "regex",
"value": "margin:\\s*1rem\\s+2rem", "value": "margin:\\s*0\\s+auto",
"message": "Set <kbd>margin: 1rem 2rem</kbd>", "message": "Establece <kbd>margin: 0 auto</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "box-model-7", "id": "box-model-7",
"title": "Padding Shorthand Notation", "title": "Border Radius",
"description": "Like margin, padding shorthand allows setting all sides at once. A single value applies to all four sides equally.", "description": "Aunque no es parte del modelo de caja clásico, <kbd>border-radius</kbd> redondea las esquinas de la caja de borde de un elemento. Usa <kbd>50%</kbd> en un elemento cuadrado para crear un círculo.",
"task": "Set <kbd>padding</kbd> to <kbd>2rem</kbd> to add equal padding on all sides.", "task": "Haz la imagen del avatar circular con <kbd>border-radius: 50%</kbd>.",
"previewHTML": "<div class=\"padded\">This box needs equal padding on all sides</div>", "previewHTML": "<article class=\"card\"><img class=\"avatar\" src=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='35' r='25' fill='%23666'/%3E%3Ccircle cx='50' cy='90' r='40' fill='%23666'/%3E%3C/svg%3E\" alt=\"Avatar\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .padded { background-color: papayawhip; border: 2px solid orange; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; text-align: center; } .avatar { width: 80px; height: 80px; background: #ddd; margin-bottom: 8px; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".padded {\n ", "codePrefix": ".avatar {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "padding: 2rem;", "solution": "border-radius: 50%;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "padding", "expected": "2rem" }, "value": { "property": "border-radius", "expected": "50%" },
"message": "Set <kbd>padding: 2rem</kbd>" "message": "Establece <kbd>border-radius: 50%</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-8", "id": "box-model-8",
"title": "Border on Specific Sides", "title": "Complete Card",
"description": "For granular control, you can target specific sides with <kbd>border-top</kbd>, <kbd>border-right</kbd>, <kbd>border-bottom</kbd>, or <kbd>border-left</kbd>.", "description": "Combinemos todo. Esta tarjeta de notificación necesita estilo para verse profesional.",
"task": "Set <kbd>border-bottom</kbd> to <kbd>4px solid dodgerblue</kbd>.", "task": "Estiliza la notificación: añade <kbd>padding: 1rem</kbd>, <kbd>border-left: 4px solid coral</kbd> y <kbd>border-radius: 4px</kbd>.",
"previewHTML": "<div class=\"line\">This element needs only a bottom border</div>", "previewHTML": "<div class=\"alert\"><strong>New message</strong><p>You have 3 unread notifications</p></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .line { padding: 1rem; background-color: aliceblue; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { background: seashell; } .alert strong { color: coral; } .alert p { margin: 4px 0 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".line {\n ", "codePrefix": ".alert {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "border-bottom: 4px solid dodgerblue;", "solution": "padding: 1rem;\n border-left: 4px solid coral;\n border-radius: 4px;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{
"type": "property_value",
"value": { "property": "padding", "expected": "1rem" },
"message": "Establece <kbd>padding: 1rem</kbd>"
},
{ {
"type": "regex", "type": "regex",
"value": "border-bottom:\\s*4px\\s+solid\\s+dodgerblue", "value": "border-left:\\s*4px\\s+solid\\s+coral",
"message": "Set <kbd>border-bottom: 4px solid dodgerblue</kbd>", "message": "Establece <kbd>border-left: 4px solid coral</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "border-radius", "expected": "4px" },
"message": "Establece <kbd>border-radius: 4px</kbd>"
} }
] ]
} }

View File

@@ -1,115 +1,100 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "units-variables", "id": "units-variables",
"title": "CSS Units & Variables", "title": "Unidades y Variables CSS",
"description": "Understand the variety of CSS measurement units and how to define and use custom properties for maintainable styles.", "description": "Comprende la variedad de unidades de medida CSS y cómo definir y usar propiedades personalizadas para estilos mantenibles.",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "units-1", "id": "units-1",
"title": "Absolute vs. Relative Units", "title": "Relative Units",
"description": "Learn the difference between px, rem, em, %, and vw/vh for flexible, responsive layouts.", "description": "CSS ofrece dos tipos de unidades: <em>absolutas</em> (como <kbd>px</kbd>) y <em>relativas</em> (como <kbd>%</kbd> y <kbd>rem</kbd>). Las unidades relativas se adaptan a su contexto, haciendo los layouts flexibles y accesibles.<br><br><strong>Unidades relativas comunes:</strong><br>• <kbd>%</kbd> Relativo al elemento padre<br>• <kbd>rem</kbd> Relativo al tamaño de fuente raíz (típicamente 16px)<br>• <kbd>em</kbd> Relativo al tamaño de fuente del elemento<br><br>Un patrón común para contenido legible: establece <kbd>width: 100%</kbd> para llenar el espacio disponible, luego <kbd>max-width: 40rem</kbd> para limitar la longitud de línea para legibilidad.",
"task": "Set the <kbd>width</kbd> of <kbd>.box</kbd> to <kbd>80%</kbd> and <kbd>max-width</kbd> to <kbd>37.5rem</kbd>.", "task": "Este texto de artículo es demasiado ancho en pantallas grandes. Añade <kbd>max-width: 40rem</kbd> para un ancho de lectura óptimo.",
"previewHTML": "<div class=\"box\">Resize me!</div>", "previewHTML": "<article class=\"article\"><h2>The Art of Typography</h2><p>Good typography is invisible. When text is set well, readers absorb information without noticing the design decisions that make it comfortable to read. Line length is crucial—too wide and eyes get lost, too narrow and reading becomes choppy.</p><p>The ideal line length is 45-75 characters per line. At typical font sizes, this works out to roughly 40rem maximum width.</p></article>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { background: #f5f5f5; padding: 1rem; }", "previewBaseCSS": "body { font-family: Georgia, serif; padding: 1rem; background: #f9f9f9; } .article { background: white; padding: 2rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .article h2 { margin: 0 0 1rem; color: #333; } .article p { margin: 0 0 1rem; line-height: 1.6; color: #444; } .article p:last-child { margin-bottom: 0; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Set flexible sizing */\n.box {", "codePrefix": ".article {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": " width: 80%;\n max-width: 37.5rem;", "solution": "max-width: 40rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ "type": "contains", "value": "width", "message": "Use <kbd>width</kbd> property", "options": { "caseSensitive": false } },
{ "type": "property_value", "value": { "property": "width", "expected": "80%" }, "message": "Set width to <kbd>80%</kbd>" },
{ "type": "contains", "value": "max-width", "message": "Use <kbd>max-width</kbd> property", "options": { "caseSensitive": false } },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "max-width", "expected": "37.5rem" }, "value": { "property": "max-width", "expected": "40rem" },
"message": "Set max-width to <kbd>37.5rem</kbd>" "message": "Establece <kbd>max-width: 40rem</kbd>"
} }
] ]
}, },
{ {
"id": "units-2", "id": "units-2",
"title": "CSS Custom Properties", "title": "CSS Variables",
"description": "Define and reuse variables (--custom properties) to centralize your theme values.", "description": "Las propiedades personalizadas CSS (variables) te permiten definir valores reutilizables. Defínelas con <kbd>--nombre</kbd> y úsalas con <kbd>var(--nombre)</kbd>. Las variables definidas en <kbd>:root</kbd> están disponibles en todas partes.",
"task": "Create a <kbd>--main-color</kbd> variable in <kbd>:root</kbd> with <kbd>#6200ee</kbd> and apply it as the border color on <kbd>.themed</kbd>.", "task": "Define <kbd>--brand: steelblue</kbd> en <kbd>:root</kbd>, luego úsala como color de <kbd>background</kbd> para <kbd>.btn</kbd>.",
"previewHTML": "<div class=\"themed\">Variable Box</div>", "previewHTML": "<div class=\"actions\"><button class=\"btn\">Subscribe</button><button class=\"btn\">Learn More</button></div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .themed { padding: 1rem; border: 0.125rem solid #ddd; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .actions { display: flex; gap: 1rem; } .btn { color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 1rem; cursor: pointer; background: #ccc; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Define and use a CSS variable */\n:root {", "codePrefix": ":root {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}\n.themed { }", "codeSuffix": "\n}\n\n.btn {\n background: var(--brand);\n}",
"solution": " --main-color: #6200ee;\n}\n.themed {\n border-color: var(--main-color);", "solution": "--brand: steelblue;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "contains", "type": "contains",
"value": "--main-color", "value": "--brand",
"message": "Define <kbd>--main-color</kbd> in :root", "message": "Define la variable <kbd>--brand</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "contains", "type": "contains",
"value": "var(--main-color)", "value": "steelblue",
"message": "Use <kbd>var(--main-color)</kbd>", "message": "Establece el valor a <kbd>steelblue</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "border", "expected": "var(--main-color)" },
"message": "Apply variable to border color",
"options": { "exact": false }
} }
] ]
}, },
{ {
"id": "units-3", "id": "units-3",
"title": "Unit Calculations (calc)", "title": "calc() Function",
"description": "Use the <kbd>calc()</kbd> function to combine different units in one expression.", "description": "La función <kbd>calc()</kbd> te permite mezclar diferentes unidades en cálculos. Esto es esencial para layouts que combinan tamaños fijos y flexibles, como un layout con barra lateral.",
"task": "Set the <kbd>width</kbd> of <kbd>.sized</kbd> to <kbd>calc(100% - 2rem)</kbd> and <kbd>min-height</kbd> to <kbd>calc(10vh + 1rem)</kbd>.", "task": "El contenido principal debe llenar el espacio restante después de la barra lateral de 200px. Establece <kbd>width: calc(100% - 200px)</kbd> en <kbd>.main</kbd>.",
"previewHTML": "<div class=\"sized\">Calc Demo</div>", "previewHTML": "<div class=\"layout\"><aside class=\"sidebar\">Sidebar<br>Navigation</aside><main class=\"main\"><h2>Main Content</h2><p>This area should fill the remaining width after accounting for the fixed-width sidebar.</p></main></div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sized { background: #e8f5e9; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .layout { display: flex; gap: 1rem; } .sidebar { width: 200px; background: #1a1a2e; color: white; padding: 1rem; border-radius: 8px; flex-shrink: 0; } .main { background: white; padding: 1rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .main h2 { margin: 0 0 8px; } .main p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Use calc for dynamic sizing */\n.sized {", "codePrefix": ".main {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": " width: calc(100% - 2rem);\n min-height: calc(10vh + 1rem);", "solution": "width: calc(100% - 200px);",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ "type": "contains", "value": "calc", "message": "Use <kbd>calc()</kbd> function", "options": { "caseSensitive": false } },
{ {
"type": "regex", "type": "regex",
"value": "width:\\s*calc\\(100% - 2rem\\)", "value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
"message": "Width should be <kbd>calc(100% - 2rem)</kbd>", "message": "Establece <kbd>width: calc(100% - 200px)</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "regex",
"value": "min-height:\\s*calc\\(10vh \\+ 1rem\\)",
"message": "Min-height should be <kbd>calc(10vh + 1rem)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "units-4", "id": "units-4",
"title": "Viewport & Responsive Units", "title": "Viewport Units",
"description": "Control layouts relative to viewport size with vw, vh, and vmin/vmax units.", "description": "Las unidades de viewport dimensionan elementos relativos a la ventana del navegador:<br>• <kbd>vw</kbd> 1% del ancho del viewport<br>• <kbd>vh</kbd> 1% de la altura del viewport<br><br>Son perfectas para secciones de pantalla completa como banners hero.",
"task": "Give <kbd>.view</kbd> a <kbd>width</kbd> of <kbd>50vw</kbd> and <kbd>height</kbd> of <kbd>20vh</kbd>.", "task": "Haz que esta sección hero llene la altura del viewport estableciendo <kbd>min-height: 100vh</kbd>.",
"previewHTML": "<div class=\"view\">Viewport Box</div>", "previewHTML": "<section class=\"hero\"><h1>Welcome</h1><p>Scroll down to explore</p></section>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .view { background: #ffe0b2; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .hero { background: linear-gradient(135deg, #1a1a2e 0%, steelblue 100%); color: white; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 2rem; } .hero h1 { margin: 0 0 1rem; font-size: 2.5rem; } .hero p { margin: 0; opacity: 0.8; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Use viewport units */\n.view {", "codePrefix": ".hero {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": " width: 50vw;\n height: 20vh;", "solution": "min-height: 100vh;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ "type": "contains", "value": "vw", "message": "Use <kbd>vw</kbd> unit", "options": { "caseSensitive": false } }, {
{ "type": "contains", "value": "vh", "message": "Use <kbd>vh</kbd> unit", "options": { "caseSensitive": false } }, "type": "property_value",
{ "type": "property_value", "value": { "property": "width", "expected": "50vw" }, "message": "Set width to <kbd>50vw</kbd>" }, "value": { "property": "min-height", "expected": "100vh" },
{ "type": "property_value", "value": { "property": "height", "expected": "20vh" }, "message": "Set height to <kbd>20vh</kbd>" } "message": "Establece <kbd>min-height: 100vh</kbd>"
}
] ]
} }
] ]

View File

@@ -1,15 +1,15 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "transitions-animations", "id": "transitions-animations",
"title": "CSS Animations", "title": "Animaciones CSS",
"description": "Bring interactivity to your UI by smoothly transitioning properties and creating keyframe-driven animations.", "description": "Añade interactividad a tu UI mediante transiciones suaves de propiedades y animaciones basadas en keyframes.",
"difficulty": "intermediate", "difficulty": "intermediate",
"lessons": [ "lessons": [
{ {
"id": "transitions-1", "id": "transitions-1",
"title": "Transitions", "title": "Transitions",
"description": "Learn how to apply <kbd>transition</kbd> to properties for smooth changes on state changes.<br><br><pre>transition: property duration;\n/* e.g. transition: background-color 0.3s; */</pre>", "description": "Aprende a aplicar <kbd>transition</kbd> a propiedades para cambios suaves en estados.<br><br><pre>transition: property duration;\n/* ej. transition: background-color 0.3s; */</pre>",
"task": "Add <kbd>transition: background-color 0.3s</kbd> to <kbd>.btn</kbd> so the color fades smoothly on hover.", "task": "Añade <kbd>transition: background-color 0.3s</kbd> para que el color cambie suavemente al hacer hover.",
"previewHTML": "<button class=\"btn\">Hover Me</button>", "previewHTML": "<button class=\"btn\">Hover Me</button>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: black; color: white; padding: 0.5rem 1rem; border: none; cursor: pointer; } .btn:hover { background: white; color: black; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: black; color: white; padding: 0.5rem 1rem; border: none; cursor: pointer; } .btn:hover { background: white; color: black; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -22,13 +22,13 @@
{ {
"type": "contains", "type": "contains",
"value": "transition", "value": "transition",
"message": "Use the <kbd>transition</kbd> property", "message": "Usa la propiedad <kbd>transition</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "regex", "type": "regex",
"value": "transition:\\s*background-color\\s*0\\.3s", "value": "transition:\\s*background-color\\s*0\\.3s",
"message": "Set <kbd>transition: background-color 0.3s</kbd>", "message": "Establece <kbd>transition: background-color 0.3s</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
@@ -36,8 +36,8 @@
{ {
"id": "transitions-2", "id": "transitions-2",
"title": "Timing Funcs", "title": "Timing Funcs",
"description": "Explore easing functions like <kbd>ease</kbd>, <kbd>linear</kbd>, <kbd>ease-in</kbd>, <kbd>ease-out</kbd> to control animation pacing.", "description": "Explora funciones de easing como <kbd>ease</kbd>, <kbd>linear</kbd>, <kbd>ease-in</kbd>, <kbd>ease-out</kbd> para controlar el ritmo de la animación.",
"task": "Set <kbd>transition-timing-function</kbd> to <kbd>ease-in-out</kbd> on <kbd>.btn</kbd>.", "task": "Establece <kbd>transition-timing-function</kbd> a <kbd>ease-in-out</kbd>.",
"previewHTML": "<button class=\"btn\">Timing</button>", "previewHTML": "<button class=\"btn\">Timing</button>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: navy; color: gold; padding: 0.5rem 1rem; border: none; cursor: pointer; transition: all 0.5s; } .btn:hover { background: gold; color: navy; transform: scale(1.1); }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: navy; color: gold; padding: 0.5rem 1rem; border: none; cursor: pointer; transition: all 0.5s; } .btn:hover { background: gold; color: navy; transform: scale(1.1); }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -50,21 +50,21 @@
{ {
"type": "contains", "type": "contains",
"value": "transition-timing-function", "value": "transition-timing-function",
"message": "Use <kbd>transition-timing-function</kbd>", "message": "Usa <kbd>transition-timing-function</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "transition-timing-function", "expected": "ease-in-out" }, "value": { "property": "transition-timing-function", "expected": "ease-in-out" },
"message": "Set timing to <kbd>ease-in-out</kbd>" "message": "Establece timing a <kbd>ease-in-out</kbd>"
} }
] ]
}, },
{ {
"id": "transitions-3", "id": "transitions-3",
"title": "Keyframes", "title": "Keyframes",
"description": "Create named animations using <kbd>@keyframes</kbd> and apply them via the <kbd>animation</kbd> shorthand.<br><br><pre>@keyframes bounce {\n 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;\n}</pre>", "description": "Crea animaciones nombradas usando <kbd>@keyframes</kbd> y aplícalas con el atajo <kbd>animation</kbd>.<br><br><pre>@keyframes bounce {\n 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;\n}</pre>",
"task": "Define a keyframe at <kbd>50%</kbd> with <kbd>transform: translateY(-20px)</kbd> and apply <kbd>animation: bounce 1s infinite</kbd> to <kbd>.ball</kbd>.", "task": "Define un keyframe en <kbd>50%</kbd> con <kbd>transform: translateY(-20px)</kbd> y aplica <kbd>animation: bounce 1s infinite</kbd> a <kbd>.ball</kbd>.",
"previewHTML": "<div class=\"ball\"></div>", "previewHTML": "<div class=\"ball\"></div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .ball { width: 50px; height: 50px; background: crimson; border-radius: 50%; margin: 2rem auto; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .ball { width: 50px; height: 50px; background: crimson; border-radius: 50%; margin: 2rem auto; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -83,19 +83,19 @@
{ {
"type": "regex", "type": "regex",
"value": "50%.*transform: translateY\\(-20px\\)", "value": "50%.*transform: translateY\\(-20px\\)",
"message": "At <kbd>50%</kbd>, use <kbd>transform: translateY(-20px)</kbd>", "message": "En <kbd>50%</kbd>, usa <kbd>transform: translateY(-20px)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "contains", "type": "contains",
"value": "animation", "value": "animation",
"message": "Use <kbd>animation</kbd> property on <kbd>.ball</kbd>", "message": "Usa la propiedad <kbd>animation</kbd> en <kbd>.ball</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "regex", "type": "regex",
"value": "animation:.*bounce.*1s.*infinite", "value": "animation:.*bounce.*1s.*infinite",
"message": "Apply <kbd>animation: bounce 1s infinite</kbd>", "message": "Aplica <kbd>animation: bounce 1s infinite</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
@@ -103,8 +103,8 @@
{ {
"id": "transitions-4", "id": "transitions-4",
"title": "Animation Properties", "title": "Animation Properties",
"description": "Fine-tune animations with <kbd>animation-delay</kbd>, <kbd>animation-iteration-count</kbd>, <kbd>animation-direction</kbd>, and <kbd>animation-fill-mode</kbd>.", "description": "Ajusta las animaciones con <kbd>animation-delay</kbd>, <kbd>animation-iteration-count</kbd>, <kbd>animation-direction</kbd> y <kbd>animation-fill-mode</kbd>.",
"task": "Apply the <kbd>pulse</kbd> animation to <kbd>.box</kbd> with <kbd>animation-name: pulse</kbd>, <kbd>animation-duration: 2s</kbd>, <kbd>animation-delay: 1s</kbd>, <kbd>animation-iteration-count: 2</kbd>, and <kbd>animation-fill-mode: forwards</kbd>.", "task": "Aplica la animación <kbd>pulse</kbd> a <kbd>.box</kbd> con <kbd>animation-name: pulse</kbd>, <kbd>animation-duration: 2s</kbd>, <kbd>animation-delay: 1s</kbd>, <kbd>animation-iteration-count: 2</kbd> y <kbd>animation-fill-mode: forwards</kbd>.",
"previewHTML": "<div class=\"box\">Pulse</div>", "previewHTML": "<div class=\"box\">Pulse</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { width: 100px; height: 100px; background: black; color: white; display: flex; align-items: center; justify-content: center; margin: 2rem auto; } @keyframes pulse { 0% { background: black; color: white; transform: scale(1); } 50% { background: white; color: black; transform: scale(1.2); } 100% { background: limegreen; color: black; transform: scale(1); } }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { width: 100px; height: 100px; background: black; color: white; display: flex; align-items: center; justify-content: center; margin: 2rem auto; } @keyframes pulse { 0% { background: black; color: white; transform: scale(1); } 50% { background: white; color: black; transform: scale(1.2); } 100% { background: limegreen; color: black; transform: scale(1); } }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -117,27 +117,27 @@
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-name", "expected": "pulse" }, "value": { "property": "animation-name", "expected": "pulse" },
"message": "Set <kbd>animation-name: pulse</kbd>" "message": "Establece <kbd>animation-name: pulse</kbd>"
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-duration", "expected": "2s" }, "value": { "property": "animation-duration", "expected": "2s" },
"message": "Set <kbd>animation-duration: 2s</kbd>" "message": "Establece <kbd>animation-duration: 2s</kbd>"
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-delay", "expected": "1s" }, "value": { "property": "animation-delay", "expected": "1s" },
"message": "Set <kbd>animation-delay: 1s</kbd>" "message": "Establece <kbd>animation-delay: 1s</kbd>"
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-iteration-count", "expected": "2" }, "value": { "property": "animation-iteration-count", "expected": "2" },
"message": "Set <kbd>animation-iteration-count: 2</kbd>" "message": "Establece <kbd>animation-iteration-count: 2</kbd>"
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-fill-mode", "expected": "forwards" }, "value": { "property": "animation-fill-mode", "expected": "forwards" },
"message": "Set <kbd>animation-fill-mode: forwards</kbd>" "message": "Establece <kbd>animation-fill-mode: forwards</kbd>"
} }
] ]
} }

View File

@@ -2,14 +2,14 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "responsive-design", "id": "responsive-design",
"title": "CSS Responsive Design", "title": "CSS Responsive Design",
"description": "Make your layouts adapt to different screen sizes using media queries and fluid design techniques.", "description": "Adapta tus layouts a diferentes tamaños de pantalla usando media queries y técnicas de diseño fluido.",
"difficulty": "intermediate", "difficulty": "intermediate",
"lessons": [ "lessons": [
{ {
"id": "responsive-1", "id": "responsive-1",
"title": "Media Queries", "title": "Media Queries",
"description": "Understand the syntax and use cases for CSS media queries to apply styles conditionally based on viewport characteristics.", "description": "Comprende la sintaxis y casos de uso de las media queries de CSS para aplicar estilos condicionalmente basándose en características del viewport.<br><br><pre>@media (max-width: 600px) {\n .panel {\n background: lightcoral;\n }\n}</pre>",
"task": "Write a media query with <kbd>@media (max-width: 600px)</kbd> that changes <kbd>.panel</kbd> background to <kbd>lightcoral</kbd>.", "task": "Escribe una media query con <kbd>@media (max-width: 600px)</kbd> que cambie el fondo de <kbd>.panel</kbd> a <kbd>lightcoral</kbd>.",
"previewHTML": "<div class=\"panel\">Resize the window</div>", "previewHTML": "<div class=\"panel\">Resize the window</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .panel { padding: 1rem; background: lightblue; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .panel { padding: 1rem; background: lightblue; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -22,19 +22,19 @@
{ {
"type": "regex", "type": "regex",
"value": "@media\\s*\\(max-width:\\s*600px\\)", "value": "@media\\s*\\(max-width:\\s*600px\\)",
"message": "Use <kbd>@media (max-width: 600px)</kbd>", "message": "Usa <kbd>@media (max-width: 600px)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "contains", "type": "contains",
"value": ".panel", "value": ".panel",
"message": "Target <kbd>.panel</kbd> inside the media query", "message": "Selecciona <kbd>.panel</kbd> dentro de la media query",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "background", "expected": "lightcoral" }, "value": { "property": "background", "expected": "lightcoral" },
"message": "Set <kbd>background: lightcoral</kbd>", "message": "Establece <kbd>background: lightcoral</kbd>",
"options": { "exact": false } "options": { "exact": false }
} }
] ]
@@ -42,8 +42,8 @@
{ {
"id": "responsive-2", "id": "responsive-2",
"title": "Fluid Type", "title": "Fluid Type",
"description": "Use relative units like <kbd>vw</kbd> to make font sizes scale with the viewport width.", "description": "Usa unidades relativas como <kbd>vw</kbd> para que los tamaños de fuente escalen con el ancho del viewport.",
"task": "Set <kbd>font-size: 5vw</kbd> on <kbd>.text</kbd> so it scales as the viewport changes.", "task": "Establece <kbd>font-size: 5vw</kbd> para que escale con el viewport.",
"previewHTML": "<p class=\"text\">Fluid Typography</p>", "previewHTML": "<p class=\"text\">Fluid Typography</p>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -53,46 +53,50 @@
"solution": " font-size: 5vw;", "solution": " font-size: 5vw;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "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": "Establece <kbd>font-size: 5vw</kbd>"
}
] ]
}, },
{ {
"id": "responsive-3", "id": "responsive-3",
"title": "Flex Grids", "title": "Responsive Grid",
"description": "Combine CSS Grid with <kbd>auto-fit</kbd> or <kbd>auto-fill</kbd> for responsive column layouts.", "description": "Combina CSS Grid con <kbd>auto-fit</kbd> o <kbd>auto-fill</kbd> para layouts de columnas responsivos que ajustan automáticamente el número de columnas según el espacio disponible.",
"task": "Add <kbd>display: grid</kbd>, <kbd>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</kbd>, and <kbd>gap: 1rem</kbd> to <kbd>.cards</kbd>.", "task": "Añade <kbd>display: grid</kbd>, <kbd>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</kbd> y <kbd>gap: 1rem</kbd>.",
"previewHTML": "<div class=\"cards\"><div>1</div><div>2</div><div>3</div><div>4</div></div>", "previewHTML": "<section class=\"features\"><article class=\"feature\"><h3>Fast</h3><p>Lightning quick load times</p></article><article class=\"feature\"><h3>Secure</h3><p>Enterprise-grade security</p></article><article class=\"feature\"><h3>Reliable</h3><p>99.9% uptime guaranteed</p></article><article class=\"feature\"><h3>Support</h3><p>24/7 customer service</p></article></section>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .cards > div { background: #d1c4e9; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .feature { background: white; padding: 1rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .feature h3 { margin: 0 0 8px; color: steelblue; } .feature p { margin: 0; color: #666; font-size: 0.9rem; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Create a responsive grid */\n.cards {", "codePrefix": ".features {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": "display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;", "solution": "display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "display", "expected": "grid" }, "value": { "property": "display", "expected": "grid" },
"message": "Set <kbd>display: grid</kbd>" "message": "Establece <kbd>display: grid</kbd>"
}, },
{ {
"type": "regex", "type": "regex",
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)", "value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
"message": "Use <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>", "message": "Usa <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "gap", "expected": "1rem" }, "value": { "property": "gap", "expected": "1rem" },
"message": "Set <kbd>gap: 1rem</kbd>" "message": "Establece <kbd>gap: 1rem</kbd>"
} }
] ]
}, },
{ {
"id": "responsive-4", "id": "responsive-4",
"title": "Mobile-First", "title": "Mobile-First",
"description": "Adopt a mobile-first approach by writing base styles for small screens and enhancing for larger viewports.", "description": "Adopta un enfoque mobile-first escribiendo estilos base para pantallas pequeñas y mejorándolos para viewports más grandes.",
"task": "Write a media query with <kbd>@media (min-width: 768px)</kbd> that sets <kbd>.sidebar</kbd> width to <kbd>250px</kbd>.", "task": "Escribe una media query con <kbd>@media (min-width: 768px)</kbd> que establezca el ancho de <kbd>.sidebar</kbd> a <kbd>250px</kbd>.",
"previewHTML": "<aside class=\"sidebar\">Sidebar</aside>", "previewHTML": "<aside class=\"sidebar\">Sidebar</aside>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -105,19 +109,19 @@
{ {
"type": "regex", "type": "regex",
"value": "@media\\s*\\(min-width:\\s*768px\\)", "value": "@media\\s*\\(min-width:\\s*768px\\)",
"message": "Use <kbd>@media (min-width: 768px)</kbd>", "message": "Usa <kbd>@media (min-width: 768px)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "contains", "type": "contains",
"value": ".sidebar", "value": ".sidebar",
"message": "Target <kbd>.sidebar</kbd> inside media query", "message": "Selecciona <kbd>.sidebar</kbd> en la media query",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "width", "expected": "250px" }, "value": { "property": "width", "expected": "250px" },
"message": "Set <kbd>width: 250px</kbd>", "message": "Establece <kbd>width: 250px</kbd>",
"options": { "exact": false } "options": { "exact": false }
} }
] ]

View File

@@ -2,65 +2,65 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-elements", "id": "html-elements",
"title": "HTML Block & Inline", "title": "HTML Block & Inline",
"description": "Understanding the fundamental difference between container (block) and inline elements", "description": "Comprende la diferencia fundamental entre elementos contenedores (bloque) y elementos en línea",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "block-vs-inline-intro", "id": "block-vs-inline-intro",
"title": "Block vs Inline Elements", "title": "Elementos de bloque vs en línea",
"description": "HTML elements fall into two main categories:<br><br><strong>Block elements</strong> (containers) start on a new line and take full width. Examples: <kbd>&lt;div&gt;</kbd>, <kbd>&lt;p&gt;</kbd>, <kbd>&lt;h1&gt;</kbd>, <kbd>&lt;section&gt;</kbd><br><br><strong>Inline elements</strong> flow within text and only take needed width. Examples: <kbd>&lt;span&gt;</kbd>, <kbd>&lt;a&gt;</kbd>, <kbd>&lt;strong&gt;</kbd>, <kbd>&lt;em&gt;</kbd>", "description": "Los elementos HTML se dividen en dos categorías principales:<br><br><strong>Elementos de bloque</strong> (contenedores) comienzan en una nueva línea y ocupan todo el ancho. Ejemplos: <kbd>&lt;div&gt;</kbd>, <kbd>&lt;p&gt;</kbd>, <kbd>&lt;h1&gt;</kbd>, <kbd>&lt;section&gt;</kbd><br><br><strong>Elementos en línea</strong> fluyen dentro del texto y solo ocupan el ancho necesario. Ejemplos: <kbd>&lt;span&gt;</kbd>, <kbd>&lt;a&gt;</kbd>, <kbd>&lt;strong&gt;</kbd>, <kbd>&lt;em&gt;</kbd>",
"task": "Wrap the word <kbd>important</kbd> with <kbd>&lt;strong&gt;</kbd> tags to make it bold. Notice how the paragraph (block) takes full width while strong (inline) flows with text.", "task": "Envuelve la palabra <kbd>importante</kbd> con etiquetas <kbd>&lt;strong&gt;</kbd> para ponerla en negrita. Observa cómo el párrafo (bloque) ocupa todo el ancho mientras que strong (en línea) fluye con el texto.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; } p { background: #e3f2fd; padding: 10px; } strong { background: #ffecb3; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; } p { background: #e3f2fd; padding: 10px; } strong { background: #ffecb3; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<p>This is a paragraph with an important word.</p>", "initialCode": "<p>Este es un párrafo con una palabra importante.</p>",
"solution": "<p>This is a paragraph with an <strong>important</strong> word.</p>", "solution": "<p>Este es un párrafo con una palabra <strong>importante</strong>.</p>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "p", "value": "p",
"message": "Add a <kbd>&lt;p&gt;</kbd> paragraph element" "message": "Añade un elemento de párrafo <kbd>&lt;p&gt;</kbd>"
}, },
{ {
"type": "parent_child", "type": "parent_child",
"value": { "parent": "p", "child": "strong" }, "value": { "parent": "p", "child": "strong" },
"message": "Wrap the word <kbd>important</kbd> with <kbd>&lt;strong&gt;</kbd> tags" "message": "Envuelve la palabra <kbd>importante</kbd> con etiquetas <kbd>&lt;strong&gt;</kbd>"
} }
] ]
}, },
{ {
"id": "semantic-containers", "id": "semantic-containers",
"title": "Semantic Tags", "title": "Etiquetas semánticas",
"description": "Modern HTML uses semantic containers that describe their content:<br><br><kbd>&lt;header&gt;</kbd> - Page or section header<br><kbd>&lt;nav&gt;</kbd> - Navigation links<br><kbd>&lt;main&gt;</kbd> - Main content area<br><kbd>&lt;section&gt;</kbd> - Thematic grouping<br><kbd>&lt;article&gt;</kbd> - Self-contained content<br><kbd>&lt;footer&gt;</kbd> - Page or section footer", "description": "El HTML moderno usa contenedores semánticos que describen su contenido:<br><br><kbd>&lt;header&gt;</kbd> - Encabezado de página o sección<br><kbd>&lt;nav&gt;</kbd> - Enlaces de navegación<br><kbd>&lt;main&gt;</kbd> - Área de contenido principal<br><kbd>&lt;section&gt;</kbd> - Agrupación temática<br><kbd>&lt;article&gt;</kbd> - Contenido independiente<br><kbd>&lt;footer&gt;</kbd> - Pie de página o sección",
"task": "Create a basic page structure:<br>1. Add a <kbd>&lt;header&gt;</kbd> with an <kbd>&lt;h1&gt;</kbd> containing the text <code>My Website</code><br>2. Add a <kbd>&lt;main&gt;</kbd> element with a paragraph saying <code>Welcome to my site!</code><br>3. Add a <kbd>&lt;footer&gt;</kbd> with a paragraph saying <code>Copyright 2026</code>", "task": "Crea una estructura básica de página:<br>1. Añade un <kbd>&lt;header&gt;</kbd> con un <kbd>&lt;h1&gt;</kbd> que contenga el texto <code>Mi Sitio Web</code><br>2. Añade un elemento <kbd>&lt;main&gt;</kbd> con un párrafo que diga <code>¡Bienvenido a mi sitio!</code><br>3. Añade un <kbd>&lt;footer&gt;</kbd> con un párrafo que diga <code>Copyright 2026</code>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; margin: 0; } header { background: #1976d2; color: white; padding: 15px; } main { padding: 20px; min-height: 100px; } footer { background: #424242; color: white; padding: 10px; text-align: center; }", "previewBaseCSS": "body { font-family: system-ui; margin: 0; } header { background: #1976d2; color: white; padding: 15px; } main { padding: 20px; min-height: 100px; } footer { background: #424242; color: white; padding: 10px; text-align: center; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<header>\n <h1>My Website</h1>\n</header>\n<main>\n <p>Welcome to my site!</p>\n</main>\n<footer>\n <p>Copyright 2026</p>\n</footer>", "solution": "<header>\n <h1>Mi Sitio Web</h1>\n</header>\n<main>\n <p>¡Bienvenido a mi sitio!</p>\n</main>\n<footer>\n <p>Copyright 2026</p>\n</footer>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "header", "value": "header",
"message": "Add a <kbd>&lt;header&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;header&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "main", "value": "main",
"message": "Add a <kbd>&lt;main&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;main&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "footer", "value": "footer",
"message": "Add a <kbd>&lt;footer&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;footer&gt;</kbd>"
}, },
{ {
"type": "parent_child", "type": "parent_child",
"value": { "parent": "header", "child": "h1" }, "value": { "parent": "header", "child": "h1" },
"message": "Add an <kbd>&lt;h1&gt;</kbd> heading inside your header" "message": "Añade un encabezado <kbd>&lt;h1&gt;</kbd> dentro de tu header"
} }
] ]
} }

View File

@@ -1,100 +1,100 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-basic", "id": "html-forms-basic",
"title": "HTML Forms", "title": "Formularios HTML",
"description": "Learn to create forms with various input types", "description": "Aprende a crear formularios con varios tipos de campos",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "form-structure", "id": "form-structure",
"title": "Form Structure", "title": "Estructura del formulario",
"description": "Every form needs a <kbd>&lt;form&gt;</kbd> wrapper. Inside, use <kbd>&lt;label&gt;</kbd> to describe inputs and <kbd>&lt;input&gt;</kbd> for user data entry.<br><br>The <kbd>for</kbd> attribute on labels should match the <kbd>id</kbd> on inputs for accessibility.", "description": "Todo formulario necesita un contenedor <kbd>&lt;form&gt;</kbd>. Dentro, usa <kbd>&lt;label&gt;</kbd> para describir campos y <kbd>&lt;input&gt;</kbd> para la entrada de datos.<br><br>El atributo <kbd>for</kbd> en los labels debe coincidir con el <kbd>id</kbd> de los inputs para accesibilidad.",
"task": "Create a form with:<br>1. A <kbd>&lt;label&gt;</kbd> with the text <code>Name:</code> and <kbd>for=\"name\"</kbd> attribute<br>2. A text <kbd>&lt;input&gt;</kbd> with <kbd>id=\"name\"</kbd> and <kbd>name=\"name\"</kbd> attributes", "task": "Crea un formulario con:<br>1. Un <kbd>&lt;label&gt;</kbd> con el texto <code>Nombre:</code> y el atributo <kbd>for=\"name\"</kbd><br>2. Un <kbd>&lt;input&gt;</kbd> de texto con los atributos <kbd>id=\"name\"</kbd> y <kbd>name=\"name\"</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-bottom: 5px; font-weight: 500; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-bottom: 5px; font-weight: 500; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<form>\n <label for=\"name\">Name:</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n</form>", "solution": "<form>\n <label for=\"name\">Nombre:</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n</form>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "form", "value": "form",
"message": "Wrap everything in a <kbd>&lt;form&gt;</kbd> element" "message": "Envuelve todo en un elemento <kbd>&lt;form&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "label", "value": "label",
"message": "Add a <kbd>&lt;label&gt;</kbd> for your input" "message": "Añade un <kbd>&lt;label&gt;</kbd> para tu campo"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "input", "value": "input",
"message": "Add an <kbd>&lt;input&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;input&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "label", "attr": "for", "value": null }, "value": { "selector": "label", "attr": "for", "value": null },
"message": "Add a <kbd>for</kbd> attribute to your label" "message": "Añade un atributo <kbd>for</kbd> a tu label"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input", "attr": "id", "value": null }, "value": { "selector": "input", "attr": "id", "value": null },
"message": "Add an <kbd>id</kbd> attribute to your input" "message": "Añade un atributo <kbd>id</kbd> a tu campo"
} }
] ]
}, },
{ {
"id": "input-types", "id": "input-types",
"title": "Input Types", "title": "Tipos de campos",
"description": "Different input types provide appropriate keyboards and validation:<br><br><kbd>type=\"text\"</kbd> - General text<br><kbd>type=\"email\"</kbd> - Email with @ validation<br><kbd>type=\"password\"</kbd> - Hidden characters<br><kbd>type=\"number\"</kbd> - Numeric keyboard<br><kbd>type=\"tel\"</kbd> - Phone keyboard", "description": "Diferentes tipos de campos proporcionan teclados apropiados y validación:<br><br><kbd>type=\"text\"</kbd> - Texto general<br><kbd>type=\"email\"</kbd> - Email con validación @<br><kbd>type=\"password\"</kbd> - Caracteres ocultos<br><kbd>type=\"number\"</kbd> - Teclado numérico<br><kbd>type=\"tel\"</kbd> - Teclado telefónico",
"task": "Create a login form with two fields:<br>1. An email field: <kbd>&lt;label for=\"email\"&gt;Email:&lt;/label&gt;</kbd> and <kbd>&lt;input type=\"email\" id=\"email\"&gt;</kbd><br>2. A password field: <kbd>&lt;label for=\"password\"&gt;Password:&lt;/label&gt;</kbd> and <kbd>&lt;input type=\"password\" id=\"password\"&gt;</kbd>", "task": "Crea un formulario de inicio de sesión con dos campos:<br>1. Campo de email: <kbd>&lt;label for=\"email\"&gt;Email:&lt;/label&gt;</kbd> y <kbd>&lt;input type=\"email\" id=\"email\"&gt;</kbd><br>2. Campo de contraseña: <kbd>&lt;label for=\"password\"&gt;Contraseña:&lt;/label&gt;</kbd> y <kbd>&lt;input type=\"password\" id=\"password\"&gt;</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<form>\n \n</form>", "initialCode": "<form>\n \n</form>",
"solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\">\n</form>", "solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <label for=\"password\">Contraseña:</label>\n <input type=\"password\" id=\"password\" name=\"password\">\n</form>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "input[type='email']", "value": "input[type='email']",
"message": "Add an input with type=\"email\"" "message": "Añade un campo con type=\"email\""
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "input[type='password']", "value": "input[type='password']",
"message": "Add an input with type=\"password\"" "message": "Añade un campo con type=\"password\""
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "label", "min": 2 }, "value": { "selector": "label", "min": 2 },
"message": "Add labels for both inputs" "message": "Añade labels para ambos campos"
} }
] ]
}, },
{ {
"id": "submit-button", "id": "submit-button",
"title": "Submit Button", "title": "Botón de envío",
"description": "Forms need a way to submit data. Use:<br><br><kbd>&lt;button type=\"submit\"&gt;</kbd> - Preferred, flexible content<br><kbd>&lt;input type=\"submit\"&gt;</kbd> - Simple text-only button<br><br>The button text should be action-oriented (e.g., <code>Sign In</code>, 'Register', 'Send').", "description": "Los formularios necesitan una forma de enviar datos. Usa:<br><br><kbd>&lt;button type=\"submit\"&gt;</kbd> - Preferido, contenido flexible<br><kbd>&lt;input type=\"submit\"&gt;</kbd> - Botón de solo texto<br><br>El texto del botón debe estar orientado a la acción (ej. <code>Iniciar Sesión</code>, 'Registrar', 'Enviar').",
"task": "Add a submit button to the form with the text <code>Sign In</code>.", "task": "Añade un botón de envío al formulario con el texto <code>Iniciar Sesión</code>.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } button { width: 100%; margin-top: 20px; padding: 10px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } button:hover { background: #1565c0; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } button { width: 100%; margin-top: 20px; padding: 10px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } button:hover { background: #1565c0; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n \n</form>", "initialCode": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Contraseña:</label>\n <input type=\"password\" id=\"password\">\n \n</form>",
"solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n \n <button type=\"submit\">Sign In</button>\n</form>", "solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Contraseña:</label>\n <input type=\"password\" id=\"password\">\n \n <button type=\"submit\">Iniciar Sesión</button>\n</form>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "button[type='submit'], input[type='submit']", "value": "button[type='submit'], input[type='submit']",
"message": "Add a submit button to your form" "message": "Añade un botón de envío a tu formulario"
}, },
{ {
"type": "element_text", "type": "element_text",
"value": { "selector": "button", "text": "Sign In" }, "value": { "selector": "button", "text": "Iniciar Sesión" },
"message": "The button should say <kbd>Sign In</kbd>" "message": "El botón debe decir <kbd>Iniciar Sesión</kbd>"
} }
] ]
} }

View File

@@ -1,110 +1,32 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-validation", "id": "html-forms-validation",
"title": "HTML Validation", "title": "Validación de formularios",
"description": "Learn HTML5 built-in form validation attributes", "description": "Usa la validación integrada de HTML5 para mejor experiencia de usuario",
"mode": "html", "mode": "html",
"difficulty": "intermediate", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "required-fields", "id": "required-fields",
"title": "Required Fields", "title": "Campos requeridos",
"description": "The <kbd>required</kbd> attribute prevents form submission if the field is empty.<br><br>Add it to any input that must be filled:<br><kbd>&lt;input type=\"text\" required&gt;</kbd><br><br>The browser shows a validation message automatically.", "description": "El atributo <kbd>required</kbd> evita el envío del formulario si el campo está vacío. ¡El navegador muestra un mensaje de validación automáticamente - sin JavaScript!<br><br>Añádelo a cualquier campo que deba rellenarse:<br><kbd>&lt;input type=\"text\" required&gt;</kbd>",
"task": "Make both the name and email fields required by adding the <kbd>required</kbd> attribute.", "task": "Haz que ambos campos (nombre y email) sean requeridos añadiendo el atributo <kbd>required</kbd> a cada campo.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-of-type { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid { border-color: #d32f2f; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 320px; } label { display: block; margin-top: 15px; margin-bottom: 5px; font-weight: 500; } label:first-of-type { margin-top: 0; } input { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; } input:focus { outline: 2px solid steelblue; border-color: transparent; } button { margin-top: 20px; padding: 10px 20px; background: steelblue; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 500; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n \n <label for=\"email\">Email: *</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <button type=\"submit\">Submit</button>\n</form>", "initialCode": "<form>\n <label for=\"name\">Nombre *</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n \n <label for=\"email\">Email *</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <button type=\"submit\">Enviar</button>\n</form>",
"solution": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n \n <label for=\"email\">Email: *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <button type=\"submit\">Submit</button>\n</form>", "solution": "<form>\n <label for=\"name\">Nombre *</label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n \n <label for=\"email\">Email *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <button type=\"submit\">Enviar</button>\n</form>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input[name='name']", "attr": "required", "value": true }, "value": { "selector": "input[name='name']", "attr": "required", "value": true },
"message": "Add the <kbd>required</kbd> attribute to the name input" "message": "Añade <kbd>required</kbd> al campo de nombre"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input[name='email']", "attr": "required", "value": true }, "value": { "selector": "input[name='email']", "attr": "required", "value": true },
"message": "Add the <kbd>required</kbd> attribute to the email input" "message": "Añade <kbd>required</kbd> al campo de email"
}
]
},
{
"id": "input-constraints",
"title": "Constraints",
"description": "Control what users can enter:<br><br><kbd>minlength</kbd> / <kbd>maxlength</kbd> - Text length limits<br><kbd>min</kbd> / <kbd>max</kbd> - Number range<br><kbd>pattern</kbd> - Regex pattern matching<br><kbd>placeholder</kbd> - Hint text (not a label!)",
"task": "Add validation to the password input:<br>1. Add <kbd>minlength=\"8\"</kbd> for minimum length<br>2. Add <kbd>maxlength=\"20\"</kbd> for maximum length<br>3. Add <kbd>placeholder=\"Enter password\"</kbd> as a hint",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } label { display: block; margin-bottom: 5px; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid:not(:placeholder-shown) { border-color: #d32f2f; } small { display: block; font-size: 12px; color: #666; margin-top: 4px; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }",
"sandboxCSS": "",
"initialCode": "<form>\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Must be 8-20 characters</small>\n \n <button type=\"submit\">Create Account</button>\n</form>",
"solution": "<form>\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\" maxlength=\"20\" placeholder=\"Enter password\" aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Must be 8-20 characters</small>\n \n <button type=\"submit\">Create Account</button>\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "minlength", "value": "8" },
"message": "Add <kbd>minlength</kbd>=\"8\" to the password input"
},
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "maxlength", "value": "20" },
"message": "Add <kbd>maxlength</kbd>=\"20\" to the password input"
},
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "placeholder", "value": null },
"message": "Add a <kbd>placeholder</kbd> to hint what to enter"
}
]
},
{
"id": "complete-registration",
"title": "Full Form",
"description": "Build a complete registration form with all validation concepts:<br><br>- Required fields marked with *<br>- Email validation (use type=\"email\")<br>- Password with length constraints<br>- Terms checkbox (required)<br>- Submit button",
"task": "Complete the registration form. Add required attributes, proper input types, and validation constraints.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 400px; background: #f5f5f5; padding: 25px; border-radius: 8px; } h2 { margin-top: 0; margin-bottom: 20px; } label { display: block; margin-top: 15px; margin-bottom: 5px; font-weight: 500; } input[type='text'], input[type='email'], input[type='password'] { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 14px; } input:focus { outline: 2px solid #1976d2; border-color: transparent; } input[type='checkbox'] { width: auto; margin-right: 8px; vertical-align: middle; } label:has(input[type='checkbox']) { display: flex; align-items: center; font-weight: normal; } button { width: 100%; margin-top: 20px; padding: 12px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 500; } button:hover { background: #1565c0; }",
"sandboxCSS": "",
"initialCode": "<form>\n <h2>Create Account</h2>\n \n <label for=\"fullname\">Full Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\">\n \n <label for=\"email\">Email *</label>\n <input id=\"email\" name=\"email\">\n \n <label for=\"password\">Password *</label>\n <input id=\"password\" name=\"password\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\">\n I agree to the Terms of Service *\n </label>\n \n <button type=\"submit\">Register</button>\n</form>",
"solution": "<form>\n <h2>Create Account</h2>\n \n <label for=\"fullname\">Full Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\" required>\n \n <label for=\"email\">Email *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <label for=\"password\">Password *</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\" required>\n I agree to the Terms of Service *\n </label>\n \n <button type=\"submit\">Register</button>\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "attribute_value",
"value": { "selector": "#fullname", "attr": "required", "value": true },
"message": "Make the full name field <kbd>required</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#email", "attr": "type", "value": "email" },
"message": "Set the email input <kbd>type=\"email\"</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#email", "attr": "required", "value": true },
"message": "Make the email field <kbd>required</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "type", "value": "password" },
"message": "Set the password input <kbd>type=\"password\"</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "required", "value": true },
"message": "Make the password field <kbd>required</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "minlength", "value": "8" },
"message": "Add <kbd>minlength</kbd>=\"8\" to password"
},
{
"type": "attribute_value",
"value": { "selector": "#terms", "attr": "required", "value": true },
"message": "Make the terms checkbox <kbd>required</kbd>"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-details-summary", "id": "html-details-summary",
"title": "HTML Details & Summary", "title": "HTML Details & Summary",
"description": "Create expandable content sections without JavaScript", "description": "Crea secciones expandibles sin JavaScript",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "details-summary-basic", "id": "details-summary-basic",
"title": "First Widget", "title": "Primer widget",
"description": "The <kbd>&lt;details&gt;</kbd> element creates a collapsible section. The <kbd>&lt;summary&gt;</kbd> provides the clickable label.<br><br>Click the summary to toggle the hidden content - no JavaScript needed!", "description": "El elemento <kbd>&lt;details&gt;</kbd> crea una sección plegable. El <kbd>&lt;summary&gt;</kbd> proporciona la etiqueta clickeable.<br><br>¡Haz clic en el resumen para mostrar el contenido oculto - sin JavaScript!",
"task": "Create a <kbd>&lt;details&gt;</kbd> element with:<br>1. A <kbd>&lt;summary&gt;</kbd> saying <code>Click to reveal</code><br>2. A <kbd>&lt;p&gt;</kbd> with the text <code>This content was hidden!</code>", "task": "Crea un elemento <kbd>&lt;details&gt;</kbd> con:<br>1. Un <kbd>&lt;summary&gt;</kbd> que diga <code>Click to reveal</code><br>2. Un <kbd>&lt;p&gt;</kbd> con el texto <code>This content was hidden!</code>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; } summary { font-weight: 600; cursor: pointer; } summary:hover { color: #1976d2; } details p { margin-top: 15px; color: #666; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; } summary { font-weight: 600; cursor: pointer; } summary:hover { color: #1976d2; } details p { margin-top: 15px; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,30 +21,30 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "details", "value": "details",
"message": "Add a <kbd>&lt;details&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;details&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "summary", "value": "summary",
"message": "Add a <kbd>&lt;summary&gt;</kbd> inside the details" "message": "Añade un <kbd>&lt;summary&gt;</kbd> dentro del details"
}, },
{ {
"type": "parent_child", "type": "parent_child",
"value": { "parent": "details", "child": "summary" }, "value": { "parent": "details", "child": "summary" },
"message": "The <kbd>&lt;summary&gt;</kbd> must be inside <kbd>&lt;details&gt;</kbd>" "message": "El <kbd>&lt;summary&gt;</kbd> debe estar dentro de <kbd>&lt;details&gt;</kbd>"
}, },
{ {
"type": "parent_child", "type": "parent_child",
"value": { "parent": "details", "child": "p" }, "value": { "parent": "details", "child": "p" },
"message": "Add a <kbd>&lt;p&gt;</kbd> inside <kbd>&lt;details&gt;</kbd> for the hidden content" "message": "Añade un <kbd>&lt;p&gt;</kbd> dentro de <kbd>&lt;details&gt;</kbd> para el contenido oculto"
} }
] ]
}, },
{ {
"id": "details-open-attribute", "id": "details-open-attribute",
"title": "Pre-expanded Details", "title": "Expandido por defecto",
"description": "By default, <kbd>&lt;details&gt;</kbd> is closed. Add the <kbd>open</kbd> attribute to show the content initially.<br><br>This is a boolean attribute - just add <kbd>open</kbd> without a value.", "description": "Por defecto, <kbd>&lt;details&gt;</kbd> está cerrado. Añade el atributo <kbd>open</kbd> para mostrar el contenido inicialmente.<br><br>Este es un atributo booleano - solo añade <kbd>open</kbd> sin valor.",
"task": "Add the <kbd>open</kbd> attribute to the <kbd>&lt;details&gt;</kbd> element to show the content by default.", "task": "Añade el atributo <kbd>open</kbd> al elemento <kbd>&lt;details&gt;</kbd> para mostrar el contenido por defecto.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; background: #f9f9f9; } summary { font-weight: 600; cursor: pointer; } details p { margin-top: 15px; color: #666; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; background: #f9f9f9; } summary { font-weight: 600; cursor: pointer; } details p { margin-top: 15px; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -55,15 +55,15 @@
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "details", "attr": "open", "value": true }, "value": { "selector": "details", "attr": "open", "value": true },
"message": "Add the <kbd>open</kbd> attribute to <kbd>&lt;details&gt;</kbd>" "message": "Añade el atributo <kbd>open</kbd> a <kbd>&lt;details&gt;</kbd>"
} }
] ]
}, },
{ {
"id": "faq-accordion", "id": "faq-accordion",
"title": "FAQ Accordion", "title": "Acordeón FAQ",
"description": "Multiple <kbd>&lt;details&gt;</kbd> elements create an accordion-style FAQ. Each question can be expanded independently.<br><br><b>Pro tip:</b> Type <kbd>details*3&gt;summary+p</kbd> and press Tab for Emmet expansion. <kbd>*3</kbd> creates 3 elements, <kbd>&gt;</kbd> nests inside, <kbd>+</kbd> adds siblings.", "description": "Múltiples elementos <kbd>&lt;details&gt;</kbd> crean un FAQ estilo acordeón. Cada pregunta puede expandirse independientemente.<br><br><b>Pro tip:</b> Escribe <kbd>details*3&gt;summary+p</kbd> y presiona Tab para expansión Emmet. <kbd>*3</kbd> crea 3 elementos, <kbd>&gt;</kbd> anida dentro, <kbd>+</kbd> añade hermanos.",
"task": "Create an FAQ section with:<br>1. An <kbd>&lt;h1&gt;</kbd> saying <code>Frequently Asked Questions</code><br>2. Three <kbd>&lt;details&gt;</kbd> elements, each with a question in <kbd>&lt;summary&gt;</kbd> and an answer in <kbd>&lt;p&gt;</kbd>", "task": "Crea una sección FAQ con:<br>1. Un <kbd>&lt;h1&gt;</kbd> que diga <code>Frequently Asked Questions</code><br>2. Tres elementos <kbd>&lt;details&gt;</kbd>, cada uno con una pregunta en <kbd>&lt;summary&gt;</kbd> y una respuesta en <kbd>&lt;p&gt;</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; min-height: 100vh; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); display: flex; flex-direction: column; justify-content: center; padding: 40px; margin: 0; box-sizing: border-box; } h1 { font-size: 2.5rem; color: #4a4a4a; text-align: center; margin: 0 0 30px 0; } details { background: white; border-radius: 12px; margin-bottom: 15px; padding: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } summary { font-size: 1.3rem; font-weight: 600; color: #5c5c5c; cursor: pointer; list-style: none; } summary::before { content: '▸ '; color: #fcb69f; } details[open] summary::before { content: '▾ '; } details p { margin: 15px 0 0 0; color: #666; line-height: 1.6; }", "previewBaseCSS": "body { font-family: system-ui; min-height: 100vh; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); display: flex; flex-direction: column; justify-content: center; padding: 40px; margin: 0; box-sizing: border-box; } h1 { font-size: 2.5rem; color: #4a4a4a; text-align: center; margin: 0 0 30px 0; } details { background: white; border-radius: 12px; margin-bottom: 15px; padding: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } summary { font-size: 1.3rem; font-weight: 600; color: #5c5c5c; cursor: pointer; list-style: none; } summary::before { content: '▸ '; color: #fcb69f; } details[open] summary::before { content: '▾ '; } details p { margin: 15px 0 0 0; color: #666; line-height: 1.6; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -74,22 +74,22 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "h1", "value": "h1",
"message": "Add an <kbd>&lt;h1&gt;</kbd> heading for the FAQ title" "message": "Añade un encabezado <kbd>&lt;h1&gt;</kbd> para el título del FAQ"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "details", "min": 3 }, "value": { "selector": "details", "min": 3 },
"message": "Create at least 3 <kbd>&lt;details&gt;</kbd> elements for the FAQ" "message": "Crea al menos 3 elementos <kbd>&lt;details&gt;</kbd> para el FAQ"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "summary", "min": 3 }, "value": { "selector": "summary", "min": 3 },
"message": "Each <kbd>&lt;details&gt;</kbd> needs a <kbd>&lt;summary&gt;</kbd> for the question" "message": "Cada <kbd>&lt;details&gt;</kbd> necesita un <kbd>&lt;summary&gt;</kbd> para la pregunta"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "details p", "min": 3 }, "value": { "selector": "details p", "min": 3 },
"message": "Each <kbd>&lt;details&gt;</kbd> needs a <kbd>&lt;p&gt;</kbd> for the answer" "message": "Cada <kbd>&lt;details&gt;</kbd> necesita un <kbd>&lt;p&gt;</kbd> para la respuesta"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-progress-meter", "id": "html-progress-meter",
"title": "HTML Progress & Meter", "title": "HTML Progress & Meter",
"description": "Display completion status and scalar measurements natively", "description": "Muestra el estado de completado y mediciones escalares nativamente",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "progress-basic", "id": "progress-basic",
"title": "Progress Bars", "title": "Barras de progreso",
"description": "The <kbd>&lt;progress&gt;</kbd> element shows task completion. Use <kbd>value</kbd> for current progress and <kbd>max</kbd> for the total.<br><br><b>Note:</b> This is not a self-closing tag! Write <kbd>&lt;progress&gt;...&lt;/progress&gt;</kbd> with fallback text inside for older browsers.", "description": "El elemento <kbd>&lt;progress&gt;</kbd> muestra el progreso de una tarea. Usa <kbd>value</kbd> para el progreso actual y <kbd>max</kbd> para el total.<br><br><b>Nota:</b> ¡Esto no es una etiqueta de cierre automático! Escribe <kbd>&lt;progress&gt;...&lt;/progress&gt;</kbd> con texto alternativo dentro para navegadores antiguos.",
"task": "Create a progress bar showing 70% completion:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Download:</code><br>2. Add a <kbd>&lt;progress&gt;</kbd> with <kbd>value=\"70\"</kbd> and <kbd>max=\"100\"</kbd>", "task": "Crea una barra de progreso mostrando 70% de completado:<br>1. Añade un <kbd>&lt;label&gt;</kbd> que diga <code>Download:</code><br>2. Añade un <kbd>&lt;progress&gt;</kbd> con <kbd>value=\"70\"</kbd> y <kbd>max=\"100\"</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } progress { width: 100%; height: 20px; border-radius: 10px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 10px; } progress::-webkit-progress-value { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; } progress::-moz-progress-bar { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } progress { width: 100%; height: 20px; border-radius: 10px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 10px; } progress::-webkit-progress-value { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; } progress::-moz-progress-bar { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,30 +21,30 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "progress", "value": "progress",
"message": "Add a <kbd>&lt;progress&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;progress&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "progress", "attr": "value", "value": "70" }, "value": { "selector": "progress", "attr": "value", "value": "70" },
"message": "Set <kbd>value=</kbd>\"70\" on the progress element" "message": "Establece <kbd>value=</kbd>\"70\" en el elemento progress"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "progress", "attr": "max", "value": "100" }, "value": { "selector": "progress", "attr": "max", "value": "100" },
"message": "Set <kbd>max=</kbd>\"100\" on the progress element" "message": "Establece <kbd>max=</kbd>\"100\" en el elemento progress"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "label", "value": "label",
"message": "Add a <kbd>&lt;label&gt;</kbd> for the progress bar" "message": "Añade un <kbd>&lt;label&gt;</kbd> para la barra de progreso"
} }
] ]
}, },
{ {
"id": "progress-indeterminate", "id": "progress-indeterminate",
"title": "Indeterminate Progress", "title": "Progreso indeterminado",
"description": "When progress is unknown (like loading), omit the <kbd>value</kbd> attribute. This creates an animated indeterminate state.<br><br>Useful for network requests or processes with unknown duration.", "description": "Cuando el progreso es desconocido (como al cargar), omite el atributo <kbd>value</kbd>. Esto crea un estado animado indeterminado.<br><br>Útil para solicitudes de red o procesos con duración desconocida.",
"task": "Create a loading indicator:<br>1. Add a <kbd>&lt;p&gt;</kbd> saying <code>Loading...</code><br>2. Add a <kbd>&lt;progress&gt;</kbd> without a value attribute", "task": "Crea un indicador de carga:<br>1. Añade un <kbd>&lt;p&gt;</kbd> que diga <code>Loading...</code><br>2. Añade un <kbd>&lt;progress&gt;</kbd> sin atributo value",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } p { margin-bottom: 10px; color: #666; } progress { width: 100%; height: 8px; border-radius: 4px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 4px; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } p { margin-bottom: 10px; color: #666; } progress { width: 100%; height: 8px; border-radius: 4px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 4px; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -55,20 +55,20 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "progress", "value": "progress",
"message": "Add a <kbd>&lt;progress&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;progress&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "p", "value": "p",
"message": "Add a <kbd>&lt;p&gt;</kbd> with loading text" "message": "Añade un <kbd>&lt;p&gt;</kbd> con texto de carga"
} }
] ]
}, },
{ {
"id": "meter-gauge", "id": "meter-gauge",
"title": "Meter Gauges", "title": "Indicadores meter",
"description": "The <kbd>&lt;meter&gt;</kbd> element displays a scalar value within a range. Use it for measurements like disk space, battery, or ratings.<br><br>Set <kbd>low</kbd>, <kbd>high</kbd>, and <kbd>optimum</kbd> to define good/bad ranges - the browser colors it accordingly!", "description": "El elemento <kbd>&lt;meter&gt;</kbd> muestra un valor escalar dentro de un rango. Úsalo para mediciones como espacio en disco, batería o calificaciones.<br><br>Establece <kbd>low</kbd>, <kbd>high</kbd> y <kbd>optimum</kbd> para definir rangos buenos/malos - ¡el navegador lo colorea correspondientemente!",
"task": "Create a battery level meter:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Battery:</code><br>2. Add a <kbd>&lt;meter&gt;</kbd> with:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> and <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> and <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>", "task": "Crea un indicador de nivel de batería:<br>1. Añade un <kbd>&lt;label&gt;</kbd> que diga <code>Battery:</code><br>2. Añade un <kbd>&lt;meter&gt;</kbd> con:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> y <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> y <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } meter { width: 100%; height: 25px; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } meter { width: 100%; height: 25px; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -79,22 +79,42 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "meter", "value": "meter",
"message": "Add a <kbd>&lt;meter&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;meter&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "meter", "attr": "value", "value": "0.8" }, "value": { "selector": "meter", "attr": "value", "value": "0.8" },
"message": "Set <kbd>value=</kbd>\"0.8\" on the meter" "message": "Establece <kbd>value=</kbd>\"0.8\" en el meter"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "min", "value": "0" },
"message": "Establece <kbd>min=</kbd>\"0\" en el meter"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "max", "value": "1" },
"message": "Establece <kbd>max=</kbd>\"1\" en el meter"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "meter", "attr": "low", "value": "0.2" }, "value": { "selector": "meter", "attr": "low", "value": "0.2" },
"message": "Set <kbd>low=</kbd>\"0.2\" to define the low threshold" "message": "Establece <kbd>low=</kbd>\"0.2\" para definir el umbral bajo"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "high", "value": "0.8" },
"message": "Establece <kbd>high=</kbd>\"0.8\" para definir el umbral alto"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "optimum", "value": "1" },
"message": "Establece <kbd>optimum=</kbd>\"1\" para indicar el valor óptimo"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "label", "value": "label",
"message": "Add a <kbd>&lt;label&gt;</kbd> for the meter" "message": "Añade un <kbd>&lt;label&gt;</kbd> para el meter"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-datalist", "id": "html-datalist",
"title": "Datalist", "title": "Datalist",
"description": "Provide suggestions for text inputs without JavaScript", "description": "Proporciona sugerencias para campos de texto sin JavaScript",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "datalist-basic", "id": "datalist-basic",
"title": "Input with Suggestions", "title": "Campo con sugerencias",
"description": "The <kbd>&lt;datalist&gt;</kbd> element provides autocomplete suggestions for inputs. Connect it using the <kbd>list</kbd> attribute on the input matching the datalist's <kbd>id</kbd>.<br><br>Users can still type freely - suggestions are just helpers!", "description": "El elemento <kbd>&lt;datalist&gt;</kbd> proporciona sugerencias de autocompletado para campos. Conéctalo usando el atributo <kbd>list</kbd> en el input que coincida con el <kbd>id</kbd> del datalist.<br><br>Los usuarios pueden escribir libremente - ¡las sugerencias son solo ayudas!",
"task": "Create a browser selector:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Browser:</code><br>2. Add an <kbd>&lt;input&gt;</kbd> with <kbd>list=\"browsers\"</kbd><br>3. Add a <kbd>&lt;datalist id=\"browsers\"&gt;</kbd> with options for Chrome, Firefox, and Safari", "task": "Crea un selector de navegador:<br>1. Añade un <kbd>&lt;label&gt;</kbd> que diga <code>Navegador:</code><br>2. Añade un <kbd>&lt;input&gt;</kbd> con <kbd>list=\"browsers\"</kbd><br>3. Añade un <kbd>&lt;datalist id=\"browsers\"&gt;</kbd> con opciones para Chrome, Firefox y Safari",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } input { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; font-size: 16px; } input:focus { outline: 2px solid #1976d2; border-color: transparent; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } input { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; font-size: 16px; } input:focus { outline: 2px solid #1976d2; border-color: transparent; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,30 +21,30 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "datalist", "value": "datalist",
"message": "Add a <kbd>&lt;datalist&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;datalist&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input", "attr": "list", "value": "browsers" }, "value": { "selector": "input", "attr": "list", "value": "browsers" },
"message": "Connect the input to datalist using <kbd>list=</kbd>\"browsers\"" "message": "Conecta el input al datalist usando <kbd>list=</kbd>\"browsers\""
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "option", "min": 3 }, "value": { "selector": "option", "min": 3 },
"message": "Add at least 3 <kbd>&lt;option&gt;</kbd> elements inside <kbd>&lt;datalist&gt;</kbd>" "message": "Añade al menos 3 elementos <kbd>&lt;option&gt;</kbd> dentro de <kbd>&lt;datalist&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "label", "value": "label",
"message": "Add a <kbd>&lt;label&gt;</kbd> for the input" "message": "Añade un <kbd>&lt;label&gt;</kbd> para el campo"
} }
] ]
}, },
{ {
"id": "datalist-countries", "id": "datalist-countries",
"title": "Country Selector", "title": "Selector de países",
"description": "Datalists work great for long lists like countries. Users can type to filter suggestions instantly.<br><br>The <kbd>value</kbd> attribute is what gets entered, and you can add display text after it.", "description": "Los datalist funcionan genial para listas largas como países. Los usuarios pueden escribir para filtrar sugerencias al instante.<br><br>El atributo <kbd>value</kbd> es lo que se ingresa, y puedes añadir texto de visualización después.",
"task": "Create a country input:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Country:</code><br>2. Add an <kbd>&lt;input&gt;</kbd> with <kbd>list=\"countries\"</kbd><br>3. Add a <kbd>&lt;datalist id=\"countries\"&gt;</kbd> with at least 4 country options", "task": "Crea un campo de país:<br>1. Añade un <kbd>&lt;label&gt;</kbd> que diga <code>País:</code><br>2. Añade un <kbd>&lt;input&gt;</kbd> con <kbd>list=\"countries\"</kbd><br>3. Añade un <kbd>&lt;datalist id=\"countries\"&gt;</kbd> con al menos 4 opciones de países",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 30px; background: linear-gradient(135deg, #e0f7fa 0%, #b2ebf2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } label { display: block; margin-bottom: 10px; font-weight: 600; color: #00695c; } input { width: 100%; padding: 12px 15px; border: 2px solid #26a69a; border-radius: 8px; font-size: 16px; background: white; } input:focus { outline: none; border-color: #00695c; box-shadow: 0 0 0 3px rgba(38,166,154,0.2); }", "previewBaseCSS": "body { font-family: system-ui; padding: 30px; background: linear-gradient(135deg, #e0f7fa 0%, #b2ebf2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } label { display: block; margin-bottom: 10px; font-weight: 600; color: #00695c; } input { width: 100%; padding: 12px 15px; border: 2px solid #26a69a; border-radius: 8px; font-size: 16px; background: white; } input:focus { outline: none; border-color: #00695c; box-shadow: 0 0 0 3px rgba(38,166,154,0.2); }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -55,22 +55,22 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "datalist", "value": "datalist",
"message": "Add a <kbd>&lt;datalist&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;datalist&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "datalist", "attr": "id", "value": "countries" }, "value": { "selector": "datalist", "attr": "id", "value": "countries" },
"message": "Set <kbd>id=</kbd>\"countries\" on the datalist" "message": "Establece <kbd>id=</kbd>\"countries\" en el datalist"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input", "attr": "list", "value": "countries" }, "value": { "selector": "input", "attr": "list", "value": "countries" },
"message": "Connect the input using <kbd>list=</kbd>\"countries\"" "message": "Conecta el input usando <kbd>list=</kbd>\"countries\""
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "option", "min": 4 }, "value": { "selector": "option", "min": 4 },
"message": "Add at least 4 country options" "message": "Añade al menos 4 opciones de países"
} }
] ]
} }

View File

@@ -1,16 +1,16 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-dialog", "id": "html-dialog",
"title": "Dialogs", "title": "Diálogos",
"description": "Create modal dialogs without JavaScript libraries", "description": "Crea diálogos modales sin bibliotecas JavaScript",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "dialog-basic", "id": "dialog-basic",
"title": "Open Dialog", "title": "Abrir diálogo",
"description": "The <kbd>&lt;dialog&gt;</kbd> element creates a native modal. Add the <kbd>open</kbd> attribute to show it.<br><br>Use <kbd>&lt;form method=\"dialog\"&gt;</kbd> inside to close it when the form submits - no JavaScript needed!", "description": "El elemento <kbd>&lt;dialog&gt;</kbd> crea un modal nativo. Añade el atributo <kbd>open</kbd> para mostrarlo.<br><br>¡Usa <kbd>&lt;form method=\"dialog\"&gt;</kbd> dentro para cerrarlo al enviar el formulario - sin JavaScript!",
"task": "Create a dialog with:<br>1. The <kbd>open</kbd> attribute to show it<br>2. An <kbd>&lt;h2&gt;</kbd> saying <code>Welcome!</code><br>3. A <kbd>&lt;p&gt;</kbd> with a greeting message<br>4. A <kbd>&lt;form method=\"dialog\"&gt;</kbd> with a close button", "task": "Crea un diálogo con:<br>1. El atributo <kbd>open</kbd> para mostrarlo<br>2. Un <kbd>&lt;h2&gt;</kbd> que diga <code>¡Bienvenido!</code><br>3. Un <kbd>&lt;p&gt;</kbd> con un mensaje de saludo<br>4. Un <kbd>&lt;form method=\"dialog\"&gt;</kbd> con un botón de cerrar",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; min-height: 200px; background: #f5f5f5; } dialog { border: none; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 25px; max-width: 400px; } dialog::backdrop { background: rgba(0,0,0,0.5); } dialog h2 { margin: 0 0 15px 0; color: #1976d2; } dialog p { color: #666; margin: 0 0 20px 0; } dialog button { background: #1976d2; color: white; border: none; padding: 10px 25px; border-radius: 6px; cursor: pointer; font-size: 16px; } dialog button:hover { background: #1565c0; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; min-height: 200px; background: #f5f5f5; } dialog { border: none; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 25px; max-width: 400px; } dialog::backdrop { background: rgba(0,0,0,0.5); } dialog h2 { margin: 0 0 15px 0; color: #1976d2; } dialog p { color: #666; margin: 0 0 20px 0; } dialog button { background: #1976d2; color: white; border: none; padding: 10px 25px; border-radius: 6px; cursor: pointer; font-size: 16px; } dialog button:hover { background: #1565c0; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,35 +21,35 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog", "value": "dialog",
"message": "Add a <kbd>&lt;dialog&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;dialog&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "dialog", "attr": "open", "value": true }, "value": { "selector": "dialog", "attr": "open", "value": true },
"message": "Add the <kbd>open</kbd> attribute to show the dialog" "message": "Añade el atributo <kbd>open</kbd> para mostrar el diálogo"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog h2", "value": "dialog h2",
"message": "Add an <kbd>&lt;h2&gt;</kbd> heading inside the dialog" "message": "Añade un encabezado <kbd>&lt;h2&gt;</kbd> dentro del diálogo"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "form[method='dialog']", "value": "form[method='dialog']",
"message": "Add a <kbd>&lt;form method=\"dialog\"&gt;</kbd> for closing" "message": "Añade un <kbd>&lt;form method=\"dialog\"&gt;</kbd> para cerrar"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog button", "value": "dialog button",
"message": "Add a close button inside the form" "message": "Añade un botón de cerrar dentro del formulario"
} }
] ]
}, },
{ {
"id": "dialog-form", "id": "dialog-form",
"title": "Dialog + Form", "title": "Diálogo + Formulario",
"description": "Dialogs can contain full forms. The <kbd>method=\"dialog\"</kbd> makes the form close the dialog on submit instead of sending data.<br><br>This pattern is perfect for confirmation dialogs, quick inputs, or settings panels.", "description": "Los diálogos pueden contener formularios completos. El <kbd>method=\"dialog\"</kbd> hace que el formulario cierre el diálogo al enviar en lugar de enviar datos.<br><br>Este patrón es perfecto para diálogos de confirmación, entradas rápidas o paneles de configuración.",
"task": "Create a confirmation dialog:<br>1. Add <kbd>open</kbd> to show it<br>2. An <kbd>&lt;h2&gt;</kbd> saying <code>Confirm Delete</code><br>3. A <kbd>&lt;p&gt;</kbd> asking <code>Are you sure?</code><br>4. A <kbd>&lt;form method=\"dialog\"&gt;</kbd> with Cancel and Delete buttons", "task": "Crea un diálogo de confirmación:<br>1. Añade <kbd>open</kbd> para mostrarlo<br>2. Un <kbd>&lt;h2&gt;</kbd> que diga <code>Confirmar eliminación</code><br>3. Un <kbd>&lt;p&gt;</kbd> preguntando <code>¿Estás seguro?</code><br>4. Un <kbd>&lt;form method=\"dialog\"&gt;</kbd> con botones Cancelar y Eliminar",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; min-height: 200px; background: linear-gradient(135deg, #fce4ec 0%, #f8bbd9 100%); } dialog { border: none; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 25px; max-width: 350px; text-align: center; } dialog h2 { margin: 0 0 10px 0; color: #c62828; } dialog p { color: #666; margin: 0 0 20px 0; } dialog form { display: flex; gap: 10px; justify-content: center; } dialog button { padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; border: none; } dialog button:first-child { background: #e0e0e0; color: #333; } dialog button:last-child { background: #c62828; color: white; } dialog button:hover { opacity: 0.9; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; min-height: 200px; background: linear-gradient(135deg, #fce4ec 0%, #f8bbd9 100%); } dialog { border: none; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 25px; max-width: 350px; text-align: center; } dialog h2 { margin: 0 0 10px 0; color: #c62828; } dialog p { color: #666; margin: 0 0 20px 0; } dialog form { display: flex; gap: 10px; justify-content: center; } dialog button { padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; border: none; } dialog button:first-child { background: #e0e0e0; color: #333; } dialog button:last-child { background: #c62828; color: white; } dialog button:hover { opacity: 0.9; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -60,22 +60,22 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog[open]", "value": "dialog[open]",
"message": "Add a <kbd>&lt;dialog&gt;</kbd> with the open attribute" "message": "Añade un <kbd>&lt;dialog&gt;</kbd> con el atributo open"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog h2", "value": "dialog h2",
"message": "Add a heading to the dialog" "message": "Añade un encabezado al diálogo"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "form[method='dialog']", "value": "form[method='dialog']",
"message": "Add a <kbd>&lt;form method=\"dialog\"&gt;</kbd> for the buttons" "message": "Añade un <kbd>&lt;form method=\"dialog\"&gt;</kbd> para los botones"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "dialog button", "min": 2 }, "value": { "selector": "dialog button", "min": 2 },
"message": "Add at least 2 buttons (Cancel and Confirm)" "message": "Añade al menos 2 botones (Cancelar y Confirmar)"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-fieldset", "id": "html-forms-fieldset",
"title": "Fieldsets", "title": "Fieldsets",
"description": "Group form controls with fieldset and legend elements", "description": "Agrupa controles de formulario con elementos fieldset y legend",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "fieldset-basic", "id": "fieldset-basic",
"title": "Grouping with Fieldset", "title": "Agrupar con Fieldset",
"description": "The <kbd>&lt;fieldset&gt;</kbd> element groups related form controls together. Add a <kbd>&lt;legend&gt;</kbd> as the first child to give the group a title.<br><br>This helps with accessibility and visual organization of complex forms.", "description": "El elemento <kbd>&lt;fieldset&gt;</kbd> agrupa controles de formulario relacionados. Añade un <kbd>&lt;legend&gt;</kbd> como primer hijo para dar un título al grupo.<br><br>Esto ayuda con la accesibilidad y organización visual de formularios complejos.",
"task": "Create a form with a fieldset:<br>1. A <kbd>&lt;form&gt;</kbd> element<br>2. A <kbd>&lt;fieldset&gt;</kbd> inside<br>3. A <kbd>&lt;legend&gt;</kbd> saying <code>Personal Info</code><br>4. Two labeled inputs for name and email", "task": "Crea un formulario con un fieldset:<br>1. Un elemento <kbd>&lt;form&gt;</kbd><br>2. Un <kbd>&lt;fieldset&gt;</kbd> dentro<br>3. Un <kbd>&lt;legend&gt;</kbd> que diga <code>Información Personal</code><br>4. Dos campos etiquetados para nombre y email",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f0f4f8; } form { max-width: 400px; } fieldset { border: 2px solid #3498db; border-radius: 10px; padding: 20px; background: white; } legend { color: #3498db; font-weight: 600; padding: 0 10px; font-size: 1.1rem; } label { display: block; margin: 15px 0 5px; color: #333; font-weight: 500; } input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; } input:focus { outline: 2px solid #3498db; border-color: transparent; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f0f4f8; } form { max-width: 400px; } fieldset { border: 2px solid #3498db; border-radius: 10px; padding: 20px; background: white; } legend { color: #3498db; font-weight: 600; padding: 0 10px; font-size: 1.1rem; } label { display: block; margin: 15px 0 5px; color: #333; font-weight: 500; } input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; } input:focus { outline: 2px solid #3498db; border-color: transparent; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,35 +21,35 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "form", "value": "form",
"message": "Add a <kbd>&lt;form&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;form&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "fieldset", "value": "fieldset",
"message": "Add a <kbd>&lt;fieldset&gt;</kbd> inside the form" "message": "Añade un <kbd>&lt;fieldset&gt;</kbd> dentro del formulario"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "legend", "value": "legend",
"message": "Add a <kbd>&lt;legend&gt;</kbd> to title your fieldset" "message": "Añade un <kbd>&lt;legend&gt;</kbd> para titular tu fieldset"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "label", "min": 2 }, "value": { "selector": "label", "min": 2 },
"message": "Add at least 2 labels" "message": "Añade al menos 2 etiquetas"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "input", "min": 2 }, "value": { "selector": "input", "min": 2 },
"message": "Add at least 2 input fields" "message": "Añade al menos 2 campos de entrada"
} }
] ]
}, },
{ {
"id": "fieldset-textarea", "id": "fieldset-textarea",
"title": "Adding Textarea", "title": "Añadiendo Textarea",
"description": "The <kbd>&lt;textarea&gt;</kbd> element creates a multi-line text input, perfect for longer content like messages or descriptions.<br><br>Use <kbd>rows</kbd> and <kbd>cols</kbd> attributes to set default size.", "description": "El elemento <kbd>&lt;textarea&gt;</kbd> crea una entrada de texto multilínea, perfecta para contenido largo como mensajes o descripciones.<br><br>Usa los atributos <kbd>rows</kbd> y <kbd>cols</kbd> para establecer el tamaño predeterminado.",
"task": "Create a contact form:<br>1. A <kbd>&lt;fieldset&gt;</kbd> with <kbd>&lt;legend&gt;</kbd> <code>Contact Us</code><br>2. A labeled <kbd>&lt;input&gt;</kbd> for email<br>3. A labeled <kbd>&lt;textarea&gt;</kbd> for the message<br>4. A submit <kbd>&lt;button&gt;</kbd>", "task": "Crea un formulario de contacto:<br>1. Un <kbd>&lt;fieldset&gt;</kbd> con <kbd>&lt;legend&gt;</kbd> <code>Contáctanos</code><br>2. Un <kbd>&lt;input&gt;</kbd> etiquetado para email<br>3. Un <kbd>&lt;textarea&gt;</kbd> etiquetado para el mensaje<br>4. Un <kbd>&lt;button&gt;</kbd> de envío",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } form { max-width: 450px; margin: 0 auto; } fieldset { border: none; border-radius: 15px; padding: 30px; background: white; box-shadow: 0 10px 40px rgba(0,0,0,0.2); } legend { color: #667eea; font-weight: 700; padding: 0; font-size: 1.5rem; margin-bottom: 10px; } label { display: block; margin: 20px 0 8px; color: #333; font-weight: 500; } input, textarea { width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 16px; box-sizing: border-box; font-family: inherit; } input:focus, textarea:focus { outline: none; border-color: #667eea; } textarea { resize: vertical; min-height: 100px; } button { margin-top: 20px; width: 100%; padding: 14px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; } button:hover { opacity: 0.9; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } form { max-width: 450px; margin: 0 auto; } fieldset { border: none; border-radius: 15px; padding: 30px; background: white; box-shadow: 0 10px 40px rgba(0,0,0,0.2); } legend { color: #667eea; font-weight: 700; padding: 0; font-size: 1.5rem; margin-bottom: 10px; } label { display: block; margin: 20px 0 8px; color: #333; font-weight: 500; } input, textarea { width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 16px; box-sizing: border-box; font-family: inherit; } input:focus, textarea:focus { outline: none; border-color: #667eea; } textarea { resize: vertical; min-height: 100px; } button { margin-top: 20px; width: 100%; padding: 14px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; } button:hover { opacity: 0.9; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -60,35 +60,35 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "fieldset", "value": "fieldset",
"message": "Add a <kbd>&lt;fieldset&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;fieldset&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "legend", "value": "legend",
"message": "Add a <kbd>&lt;legend&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;legend&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "textarea", "value": "textarea",
"message": "Add a <kbd>&lt;textarea&gt;</kbd> for the message" "message": "Añade un <kbd>&lt;textarea&gt;</kbd> para el mensaje"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "button", "value": "button",
"message": "Add a submit button" "message": "Añade un botón de envío"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "input", "value": "input",
"message": "Add an input field for email" "message": "Añade un campo de entrada para el email"
} }
] ]
}, },
{ {
"id": "fieldset-multiple", "id": "fieldset-multiple",
"title": "Multiple Fieldsets", "title": "Múltiples Fieldsets",
"description": "Complex forms can use multiple <kbd>&lt;fieldset&gt;</kbd> elements to organize different sections.<br><br>This improves usability for long forms like registration or checkout pages.", "description": "Los formularios complejos pueden usar múltiples elementos <kbd>&lt;fieldset&gt;</kbd> para organizar diferentes secciones.<br><br>Esto mejora la usabilidad en formularios largos como registro o checkout.",
"task": "Create a registration form with 2 fieldsets:<br>1. <code>Account Info</code> with username and password inputs<br>2. <code>Preferences</code> with a textarea for bio<br>3. A submit button outside the fieldsets", "task": "Crea un formulario de registro con 2 fieldsets:<br>1. <code>Información de Cuenta</code> con campos de usuario y contraseña<br>2. <code>Preferencias</code> con un textarea para bio<br>3. Un botón de envío fuera de los fieldsets",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #fafafa; } form { max-width: 500px; } fieldset { border: 1px solid #ddd; border-radius: 10px; padding: 20px; margin-bottom: 20px; background: white; } legend { color: #2c3e50; font-weight: 600; padding: 0 10px; } label { display: block; margin: 15px 0 5px; color: #555; } input, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; font-family: inherit; } input:focus, textarea:focus { outline: 2px solid #3498db; border-color: transparent; } textarea { resize: vertical; min-height: 80px; } button { width: 100%; padding: 14px; background: #2c3e50; color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; } button:hover { background: #34495e; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #fafafa; } form { max-width: 500px; } fieldset { border: 1px solid #ddd; border-radius: 10px; padding: 20px; margin-bottom: 20px; background: white; } legend { color: #2c3e50; font-weight: 600; padding: 0 10px; } label { display: block; margin: 15px 0 5px; color: #555; } input, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; font-family: inherit; } input:focus, textarea:focus { outline: 2px solid #3498db; border-color: transparent; } textarea { resize: vertical; min-height: 80px; } button { width: 100%; padding: 14px; background: #2c3e50; color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; } button:hover { background: #34495e; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -99,27 +99,27 @@
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "fieldset", "min": 2 }, "value": { "selector": "fieldset", "min": 2 },
"message": "Create at least 2 fieldsets" "message": "Crea al menos 2 fieldsets"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "legend", "min": 2 }, "value": { "selector": "legend", "min": 2 },
"message": "Add a legend to each fieldset" "message": "Añade un legend a cada fieldset"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "textarea", "value": "textarea",
"message": "Add a textarea for the bio" "message": "Añade un textarea para la bio"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "button", "value": "button",
"message": "Add a submit button" "message": "Añade un botón de envío"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "input", "min": 2 }, "value": { "selector": "input", "min": 2 },
"message": "Add at least 2 input fields" "message": "Añade al menos 2 campos de entrada"
} }
] ]
} }

View File

@@ -1,125 +1,42 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-tables", "id": "html-tables",
"title": "HTML Tables", "title": "Tablas HTML",
"description": "Create structured data tables with headers and captions", "description": "Crea tablas de datos estructuradas con marcado semántico",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "table-basic", "id": "table-basic",
"title": "Basic Table Structure", "title": "Tablas de datos",
"description": "Tables use <kbd>&lt;table&gt;</kbd> with <kbd>&lt;tr&gt;</kbd> for rows. Inside rows, use <kbd>&lt;th&gt;</kbd> for headers and <kbd>&lt;td&gt;</kbd> for data cells.<br><br>The <kbd>&lt;caption&gt;</kbd> element provides an accessible title for the table.", "description": "Las tablas muestran datos estructurados en filas y columnas. Usa <kbd>&lt;table&gt;</kbd> como contenedor, <kbd>&lt;tr&gt;</kbd> para filas, <kbd>&lt;th&gt;</kbd> para celdas de encabezado y <kbd>&lt;td&gt;</kbd> para celdas de datos.<br><br>Añade <kbd>&lt;caption&gt;</kbd> para un título accesible que describa el contenido de la tabla.",
"task": "Create a simple table with:<br>1. A <kbd>&lt;caption&gt;</kbd> saying <code>Fruit Prices</code><br>2. A header row with <code>Fruit</code> and <code>Price</code> columns<br>3. At least 2 data rows", "task": "Crea una tabla de precios:<br>1. Un <kbd>&lt;caption&gt;</kbd> que diga <code>Pricing</code><br>2. Una fila de encabezado con <code>Plan</code> y <code>Price</code><br>3. Dos filas de datos para Basic ($9) y Pro ($29)",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px; font-weight: 600; font-size: 1.2rem; color: #333; background: #f8f9fa; } th, td { padding: 12px 20px; text-align: left; border-bottom: 1px solid #eee; } th { background: #3498db; color: white; font-weight: 500; } tr:hover { background: #f8f9fa; } tr:last-child td { border-bottom: none; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; } table { border-collapse: collapse; width: 100%; max-width: 350px; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 16px; font-weight: 600; font-size: 1.1rem; color: #333; background: #f8f9fa; } th, td { padding: 14px 20px; text-align: left; border-bottom: 1px solid #eee; } th { background: steelblue; color: white; font-weight: 500; } tr:last-child td { border-bottom: none; } tr:hover td { background: #f8f9fa; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<table>\n <caption>Fruit Prices</caption>\n <tr>\n <th>Fruit</th>\n <th>Price</th>\n </tr>\n <tr>\n <td>Apple</td>\n <td>$1.50</td>\n </tr>\n <tr>\n <td>Banana</td>\n <td>$0.75</td>\n </tr>\n</table>", "solution": "<table>\n <caption>Pricing</caption>\n <tr>\n <th>Plan</th>\n <th>Price</th>\n </tr>\n <tr>\n <td>Basic</td>\n <td>$9</td>\n </tr>\n <tr>\n <td>Pro</td>\n <td>$29</td>\n </tr>\n</table>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "table", "value": "table",
"message": "Add a <kbd>&lt;table&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;table&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "caption", "value": "caption",
"message": "Add a <kbd>&lt;caption&gt;</kbd> for the table title" "message": "Añade un <kbd>&lt;caption&gt;</kbd> para el título de la tabla"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "th", "min": 2 }, "value": { "selector": "th", "min": 2 },
"message": "Add at least 2 header cells (th)" "message": "Añade celdas de encabezado (<kbd>&lt;th&gt;</kbd>) para Plan y Price"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "tr", "min": 3 }, "value": { "selector": "tr", "min": 3 },
"message": "Add at least 3 rows (1 header + 2 data rows)" "message": "Añade 3 filas (1 encabezado + 2 filas de datos)"
}
]
},
{
"id": "table-thead-tbody",
"title": "Table Head & Body",
"description": "Use <kbd>&lt;thead&gt;</kbd> to group header rows and <kbd>&lt;tbody&gt;</kbd> to group data rows. This helps browsers and assistive technology understand the table structure.<br><br>You can also use <kbd>&lt;tfoot&gt;</kbd> for footer rows like totals.",
"task": "Create a structured table:<br>1. A <kbd>&lt;caption&gt;</kbd> with <code>Monthly Sales</code><br>2. A <kbd>&lt;thead&gt;</kbd> with Month and Revenue headers<br>3. A <kbd>&lt;tbody&gt;</kbd> with at least 2 data rows",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } table { border-collapse: collapse; width: 100%; max-width: 450px; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.2); } caption { padding: 20px; font-weight: 700; font-size: 1.3rem; color: white; background: transparent; text-shadow: 0 2px 4px rgba(0,0,0,0.2); caption-side: top; } thead { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } th { padding: 15px 20px; text-align: left; color: white; font-weight: 500; } tbody tr { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } td { padding: 15px 20px; color: #333; } tbody tr:last-child { border-bottom: none; }",
"sandboxCSS": "",
"initialCode": "",
"solution": "<table>\n <caption>Monthly Sales</caption>\n <thead>\n <tr>\n <th>Month</th>\n <th>Revenue</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>January</td>\n <td>$12,500</td>\n </tr>\n <tr>\n <td>February</td>\n <td>$14,200</td>\n </tr>\n </tbody>\n</table>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "table",
"message": "Add a <kbd>&lt;table&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "caption",
"message": "Add a <kbd>&lt;caption&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "thead",
"message": "Add a <kbd>&lt;thead&gt;</kbd> for the header section"
},
{
"type": "element_exists",
"value": "tbody",
"message": "Add a <kbd>&lt;tbody&gt;</kbd> for the data rows"
},
{
"type": "element_count",
"value": { "selector": "tbody tr", "min": 2 },
"message": "Add at least 2 data rows in tbody"
}
]
},
{
"id": "table-complete",
"title": "Complete Table with Footer",
"description": "Add <kbd>&lt;tfoot&gt;</kbd> to create a footer section for totals or summary data. The footer stays at the bottom even if tbody has many rows.<br><br>Combine all sections for a fully structured, accessible table.",
"task": "Create a complete table:<br>1. A <kbd>&lt;caption&gt;</kbd> with <code>Order Summary</code><br>2. A <kbd>&lt;thead&gt;</kbd> with Item and Price headers<br>3. A <kbd>&lt;tbody&gt;</kbd> with 2 items<br>4. A <kbd>&lt;tfoot&gt;</kbd> with a Total row",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #fafafa; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px 20px; font-weight: 600; font-size: 1.1rem; color: #333; text-align: left; background: #f8f9fa; border-bottom: 2px solid #eee; } th, td { padding: 12px 20px; text-align: left; } thead th { background: #2c3e50; color: white; } tbody td { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } tfoot { background: #ecf0f1; font-weight: 600; } tfoot td { border-top: 2px solid #2c3e50; color: #2c3e50; }",
"sandboxCSS": "",
"initialCode": "",
"solution": "<table>\n <caption>Order Summary</caption>\n <thead>\n <tr>\n <th>Item</th>\n <th>Price</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>Widget</td>\n <td>$25.00</td>\n </tr>\n <tr>\n <td>Gadget</td>\n <td>$35.00</td>\n </tr>\n </tbody>\n <tfoot>\n <tr>\n <td>Total</td>\n <td>$60.00</td>\n </tr>\n </tfoot>\n</table>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "table",
"message": "Add a <kbd>&lt;table&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "caption",
"message": "Add a <kbd>&lt;caption&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "thead",
"message": "Add a <kbd>&lt;thead&gt;</kbd> section"
},
{
"type": "element_exists",
"value": "tbody",
"message": "Add a <kbd>&lt;tbody&gt;</kbd> section"
},
{
"type": "element_exists",
"value": "tfoot",
"message": "Add a <kbd>&lt;tfoot&gt;</kbd> section for the total"
},
{
"type": "element_count",
"value": { "selector": "tbody tr", "min": 2 },
"message": "Add at least 2 item rows in tbody"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-marquee", "id": "html-marquee",
"title": "HTML Marquee", "title": "HTML Marquee",
"description": "Create scrolling text with the classic (deprecated but fun!) marquee element", "description": "Crea texto desplazable con el clásico (¡obsoleto pero divertido!) elemento marquee",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "marquee-basic", "id": "marquee-basic",
"title": "Scrolling Text", "title": "Texto Desplazable",
"description": "The <kbd>&lt;marquee&gt;</kbd> element creates scrolling text - a classic from the early web! While deprecated, it still works in most browsers.<br><br>Note: For production, use CSS animations instead. But for learning and fun, marquee is great!", "description": "El elemento <kbd>&lt;marquee&gt;</kbd> crea texto desplazable - ¡un clásico de la web temprana! Aunque está obsoleto, todavía funciona en la mayoría de navegadores.<br><br>Nota: Para producción, usa animaciones CSS. ¡Pero para aprender y divertirse, marquee es genial!",
"task": "Create a simple marquee:<br>1. Add a <kbd>&lt;marquee&gt;</kbd> element<br>2. Put some text inside like <code>Welcome to my website!</code>", "task": "Crea un marquee simple:<br>1. Añade un elemento <kbd>&lt;marquee&gt;</kbd><br>2. Pon texto dentro como <code>¡Bienvenido a mi sitio web!</code>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2rem; color: #00ff00; text-shadow: 0 0 10px #00ff00, 0 0 20px #00ff00; font-family: 'Courier New', monospace; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2rem; color: #00ff00; text-shadow: 0 0 10px #00ff00, 0 0 20px #00ff00; font-family: 'Courier New', monospace; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,15 +21,15 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "marquee", "value": "marquee",
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;marquee&gt;</kbd>"
} }
] ]
}, },
{ {
"id": "marquee-direction", "id": "marquee-direction",
"title": "Direction & Behavior", "title": "Dirección y Comportamiento",
"description": "Control the marquee with attributes:<br>• <kbd>direction</kbd>: left, right, up, down<br>• <kbd>behavior</kbd>: scroll (default), slide (stops at edge), alternate (bounces)<br>• <kbd>scrollamount</kbd>: speed (default is 6)", "description": "Controla el marquee con atributos:<br>• <kbd>direction</kbd>: left, right, up, down<br>• <kbd>behavior</kbd>: scroll (predeterminado), slide (se detiene en el borde), alternate (rebota)<br>• <kbd>scrollamount</kbd>: velocidad (predeterminado es 6)",
"task": "Create a bouncing marquee:<br>1. Add a <kbd>&lt;marquee&gt;</kbd> element<br>2. Set <kbd>behavior=\"alternate\"</kbd> to make it bounce<br>3. Add some fun text", "task": "Crea un marquee que rebota:<br>1. Añade un elemento <kbd>&lt;marquee&gt;</kbd><br>2. Pon <kbd>behavior=\"alternate\"</kbd> para hacerlo rebotar<br>3. Añade texto divertido",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2.5rem; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); font-weight: bold; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2.5rem; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); font-weight: bold; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -40,20 +40,20 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "marquee", "value": "marquee",
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;marquee&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "marquee", "attr": "behavior", "value": "alternate" }, "value": { "selector": "marquee", "attr": "behavior", "value": "alternate" },
"message": "Add <kbd>behavior=</kbd>\"alternate\" to make it bounce" "message": "Añade <kbd>behavior=</kbd>\"alternate\" para hacerlo rebotar"
} }
] ]
}, },
{ {
"id": "marquee-retro", "id": "marquee-retro",
"title": "Retro News Ticker", "title": "Ticker de Noticias Retro",
"description": "Combine multiple marquee attributes for a classic news ticker effect. You can even put multiple elements inside!<br><br>Remember: This is deprecated HTML. Modern sites use CSS animations, but marquee is great for understanding web history.", "description": "Combina múltiples atributos de marquee para un efecto clásico de ticker de noticias. ¡Puedes poner múltiples elementos dentro!<br><br>Recuerda: Esto es HTML obsoleto. Los sitios modernos usan animaciones CSS, pero marquee es genial para entender la historia de la web.",
"task": "Create a news ticker:<br>1. A <kbd>&lt;marquee&gt;</kbd> with <kbd>direction=\"left\"</kbd><br>2. Set <kbd>scrollamount=\"5\"</kbd> for smooth scrolling<br>3. Add a breaking news headline inside", "task": "Crea un ticker de noticias:<br>1. Un <kbd>&lt;marquee&gt;</kbd> con <kbd>direction=\"left\"</kbd><br>2. Pon <kbd>scrollamount=\"5\"</kbd> para desplazamiento suave<br>3. Añade un titular de última hora dentro",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 0; margin: 0; background: #1a1a2e; } marquee { background: linear-gradient(90deg, #c0392b 0%, #e74c3c 50%, #c0392b 100%); padding: 15px 0; font-size: 1.3rem; color: white; font-weight: 500; text-transform: uppercase; letter-spacing: 1px; border-top: 3px solid #f1c40f; border-bottom: 3px solid #f1c40f; }", "previewBaseCSS": "body { font-family: system-ui; padding: 0; margin: 0; background: #1a1a2e; } marquee { background: linear-gradient(90deg, #c0392b 0%, #e74c3c 50%, #c0392b 100%); padding: 15px 0; font-size: 1.3rem; color: white; font-weight: 500; text-transform: uppercase; letter-spacing: 1px; border-top: 3px solid #f1c40f; border-bottom: 3px solid #f1c40f; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -64,17 +64,17 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "marquee", "value": "marquee",
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;marquee&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "marquee", "attr": "direction", "value": "left" }, "value": { "selector": "marquee", "attr": "direction", "value": "left" },
"message": "Add <kbd>direction=</kbd>\"left\" for horizontal scrolling" "message": "Añade <kbd>direction=</kbd>\"left\" para desplazamiento horizontal"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "marquee", "attr": "scrollamount", "value": "5" }, "value": { "selector": "marquee", "attr": "scrollamount", "value": "5" },
"message": "Add <kbd>scrollamount=</kbd>\"5\" for smooth speed" "message": "Añade <kbd>scrollamount=</kbd>\"5\" para velocidad suave"
} }
] ]
} }

View File

@@ -2,99 +2,169 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-svg", "id": "html-svg",
"title": "HTML SVG", "title": "HTML SVG",
"description": "Draw scalable vector graphics directly in HTML", "description": "Dibuja gráficos vectoriales escalables directamente en HTML",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "svg-circle", "id": "svg-circle",
"title": "Drawing Circles", "title": "Dibujando círculos",
"description": "SVG (Scalable Vector Graphics) lets you draw shapes directly in HTML. The <kbd>&lt;svg&gt;</kbd> element is the container, with <kbd>width</kbd> and <kbd>height</kbd> attributes.<br><br>Use <kbd>&lt;circle&gt;</kbd> with <kbd>cx</kbd>, <kbd>cy</kbd> (center) and <kbd>r</kbd> (radius) to draw circles.", "description": "SVG (Scalable Vector Graphics) permite dibujar formas directamente en HTML. El elemento <kbd>&lt;svg&gt;</kbd> es el contenedor con atributos <kbd>width</kbd> y <kbd>height</kbd>.<br><br>Usa <kbd>&lt;circle&gt;</kbd> con <kbd>cx</kbd>, <kbd>cy</kbd> (centro) y <kbd>r</kbd> (radio) para dibujar círculos.",
"task": "Create an SVG with a circle:<br>1. An <kbd>&lt;svg&gt;</kbd> with width=\"200\" and height=\"200\"<br>2. A <kbd>&lt;circle&gt;</kbd> centered at (100,100) with radius 50<br>3. Add a <kbd>fill</kbd> color", "task": "Crea un SVG con un círculo:<br>1. Un <kbd>&lt;svg&gt;</kbd> con width=\"200\" y height=\"200\"<br>2. Un <kbd>&lt;circle&gt;</kbd> centrado en (100,100) con radio 50<br>3. Añade un color <kbd>fill</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; display: flex; justify-content: center; } svg { background: white; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; display: flex; justify-content: center; } svg { background: white; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"#3498db\" />\n</svg>", "solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"steelblue\" />\n</svg>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "svg", "value": "svg",
"message": "Add an <kbd>&lt;svg&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;svg&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "circle", "value": "circle",
"message": "Add a <kbd>&lt;circle&gt;</kbd> element inside the SVG" "message": "Añade un elemento <kbd>&lt;circle&gt;</kbd> dentro del SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "width", "value": "200" },
"message": "Establece <kbd>width=</kbd>\"200\" en el SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "height", "value": "200" },
"message": "Establece <kbd>height=</kbd>\"200\" en el SVG"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "circle", "attr": "cx", "value": "100" }, "value": { "selector": "circle", "attr": "cx", "value": "100" },
"message": "Set <kbd>cx=</kbd>\"100\" for the circle's horizontal center" "message": "Establece <kbd>cx=</kbd>\"100\" para el centro horizontal del círculo"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "circle", "attr": "cy", "value": "100" }, "value": { "selector": "circle", "attr": "cy", "value": "100" },
"message": "Set <kbd>cy=</kbd>\"100\" for the circle's vertical center" "message": "Establece <kbd>cy=</kbd>\"100\" para el centro vertical del círculo"
},
{
"type": "attribute_value",
"value": { "selector": "circle", "attr": "r", "value": "50" },
"message": "Establece <kbd>r=</kbd>\"50\" para el radio del círculo"
} }
] ]
}, },
{ {
"id": "svg-rect-line", "id": "svg-rect-line",
"title": "Rectangles & Lines", "title": "Rectángulos y líneas",
"description": "Draw rectangles with <kbd>&lt;rect&gt;</kbd> using <kbd>x</kbd>, <kbd>y</kbd>, <kbd>width</kbd>, <kbd>height</kbd>.<br><br>Draw lines with <kbd>&lt;line&gt;</kbd> using <kbd>x1</kbd>, <kbd>y1</kbd> (start) and <kbd>x2</kbd>, <kbd>y2</kbd> (end). Lines need a <kbd>stroke</kbd> color!", "description": "Dibuja rectángulos con <kbd>&lt;rect&gt;</kbd> usando <kbd>x</kbd>, <kbd>y</kbd>, <kbd>width</kbd>, <kbd>height</kbd>.<br><br>Dibuja líneas con <kbd>&lt;line&gt;</kbd> usando <kbd>x1</kbd>, <kbd>y1</kbd> (inicio) y <kbd>x2</kbd>, <kbd>y2</kbd> (fin). ¡Las líneas necesitan un color <kbd>stroke</kbd>!",
"task": "Create an SVG with:<br>1. An <kbd>&lt;svg&gt;</kbd> (200x150)<br>2. A <kbd>&lt;rect&gt;</kbd> at position (20,20) with size 80x60<br>3. A <kbd>&lt;line&gt;</kbd> from (120,30) to (180,90) with a stroke color", "task": "Crea un SVG con:<br>1. Un <kbd>&lt;svg&gt;</kbd> (200x150)<br>2. Un <kbd>&lt;rect&gt;</kbd> en posición (20,20) con tamaño 80x60<br>3. Una <kbd>&lt;line&gt;</kbd> de (120,30) a (180,90) con color stroke",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 200px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 200px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<svg width=\"200\" height=\"150\">\n <rect x=\"20\" y=\"20\" width=\"80\" height=\"60\" fill=\"#e74c3c\" />\n <line x1=\"120\" y1=\"30\" x2=\"180\" y2=\"90\" stroke=\"#2c3e50\" stroke-width=\"3\" />\n</svg>", "solution": "<svg width=\"200\" height=\"150\">\n <rect x=\"20\" y=\"20\" width=\"80\" height=\"60\" fill=\"tomato\" />\n <line x1=\"120\" y1=\"30\" x2=\"180\" y2=\"90\" stroke=\"slategray\" stroke-width=\"3\" />\n</svg>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "svg", "value": "svg",
"message": "Add an <kbd>&lt;svg&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;svg&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "rect", "value": "rect",
"message": "Add a <kbd>&lt;rect&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;rect&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "line", "value": "line",
"message": "Add a <kbd>&lt;line&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;line&gt;</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "width", "value": "200" },
"message": "Establece <kbd>width=</kbd>\"200\" en el SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "height", "value": "150" },
"message": "Establece <kbd>height=</kbd>\"150\" en el SVG"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "x", "value": "20" },
"message": "Establece <kbd>x=</kbd>\"20\" en el rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "y", "value": "20" },
"message": "Establece <kbd>y=</kbd>\"20\" en el rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "width", "value": "80" },
"message": "Establece <kbd>width=</kbd>\"80\" en el rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "height", "value": "60" },
"message": "Establece <kbd>height=</kbd>\"60\" en el rect"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "x1", "value": "120" },
"message": "Establece <kbd>x1=</kbd>\"120\" en la line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "y1", "value": "30" },
"message": "Establece <kbd>y1=</kbd>\"30\" en la line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "x2", "value": "180" },
"message": "Establece <kbd>x2=</kbd>\"180\" en la line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "y2", "value": "90" },
"message": "Establece <kbd>y2=</kbd>\"90\" en la line"
},
{
"type": "contains",
"value": "stroke",
"message": "Añade un color <kbd>stroke</kbd> a la line"
} }
] ]
}, },
{ {
"id": "svg-shapes", "id": "svg-shapes",
"title": "Multiple Shapes", "title": "Múltiples formas",
"description": "Combine shapes to create simple graphics! Add <kbd>stroke</kbd> for outlines and <kbd>stroke-width</kbd> for thickness.<br><br>Use <kbd>fill=\"none\"</kbd> for hollow shapes. Shapes stack in order - later elements appear on top.", "description": "¡Combina formas para crear gráficos simples! Añade <kbd>stroke</kbd> para contornos y <kbd>stroke-width</kbd> para el grosor.<br><br>Usa <kbd>fill=\"none\"</kbd> para formas huecas. Las formas se apilan en orden - los elementos posteriores aparecen encima.",
"task": "Create a simple face:<br>1. An <kbd>&lt;svg&gt;</kbd> (200x200)<br>2. A large <kbd>&lt;circle&gt;</kbd> for the face<br>3. Two small <kbd>&lt;circle&gt;</kbd> elements for eyes<br>4. A <kbd>&lt;line&gt;</kbd> for the smile", "task": "Crea una cara simple:<br>1. Un <kbd>&lt;svg&gt;</kbd> (200x200)<br>2. Un <kbd>&lt;circle&gt;</kbd> grande para la cara<br>3. Dos <kbd>&lt;circle&gt;</kbd> pequeños para los ojos<br>4. Una <kbd>&lt;line&gt;</kbd> para la sonrisa",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); min-height: 250px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); min-height: 250px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"80\" fill=\"#f1c40f\" stroke=\"#e67e22\" stroke-width=\"4\" />\n <circle cx=\"70\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <circle cx=\"130\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <line x1=\"60\" y1=\"130\" x2=\"140\" y2=\"130\" stroke=\"#2c3e50\" stroke-width=\"4\" stroke-linecap=\"round\" />\n</svg>", "solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"80\" fill=\"gold\" stroke=\"orange\" stroke-width=\"4\" />\n <circle cx=\"70\" cy=\"80\" r=\"10\" fill=\"darkslategray\" />\n <circle cx=\"130\" cy=\"80\" r=\"10\" fill=\"darkslategray\" />\n <line x1=\"60\" y1=\"130\" x2=\"140\" y2=\"130\" stroke=\"darkslategray\" stroke-width=\"4\" stroke-linecap=\"round\" />\n</svg>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "svg", "value": "svg",
"message": "Add an <kbd>&lt;svg&gt;</kbd> element" "message": "Añade un elemento <kbd>&lt;svg&gt;</kbd>"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "circle", "min": 3 }, "value": { "selector": "circle", "min": 3 },
"message": "Add at least 3 circles (1 face + 2 eyes)" "message": "Añade al menos 3 círculos (1 cara + 2 ojos)"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "line", "value": "line",
"message": "Add a <kbd>&lt;line&gt;</kbd> for the smile" "message": "Añade una <kbd>&lt;line&gt;</kbd> para la sonrisa"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "welcome", "id": "welcome",
"title": "Code Crispies", "title": "Code Crispies",
"description": "Welcome to Code Crispies - your interactive web development learning platform", "description": "Witaj w Code Crispies - twojej interaktywnej platformie do nauki tworzenia stron",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "get-started", "id": "get-started",
"title": "Get Started", "title": "Rozpocznij",
"description": "<strong>Code Crispies</strong> is a free, open-source platform for learning web development through hands-on exercises. No account required!<br><br><strong>What you'll learn:</strong><br>• <strong>HTML</strong> - Semantic elements, forms, tables, SVG (<em>HTML Block & Inline</em>, <em>HTML Forms</em>, <em>HTML Tables</em>)<br>• <strong>CSS</strong> - Selectors, box model, flexbox, animations (<em>CSS Selectors</em>, <em>CSS Box Model</em>, <em>CSS Flexbox</em>)<br>• <strong>Responsive Design</strong> - Media queries and mobile-first layouts<br><br><strong>How it works:</strong><br>1. Read the task in the left panel<br>2. Write code in the editor<br>3. See live results in the preview<br>4. Get instant feedback with hints<br><br><strong>Keyboard shortcuts:</strong> <kbd>Ctrl+Z</kbd> to undo, <kbd>Ctrl+Shift+Z</kbd> to redo<br><br><strong>More resources:</strong><br>• <a href=\"https://nextlevelshit.github.io/html-over-js/\" target=\"_blank\">HTML over JS</a> - Native HTML vs JavaScript solutions<br>• <a href=\"https://nextlevelshit.github.io/web-engineering-mandala/\" target=\"_blank\">Web Engineering Mandala</a> - JavaScript technology roadmap", "description": "<strong>Code Crispies</strong> to darmowa platforma open-source do nauki tworzenia stron przez praktyczne ćwiczenia. Konto nie jest wymagane!<br><br><strong>Czego się nauczysz:</strong><br>• <strong>HTML</strong> - Elementy semantyczne, formularze, tabele, SVG (<em>HTML Blokowe i Liniowe</em>, <em>HTML Formularze</em>, <em>HTML Tabele</em>)<br>• <strong>CSS</strong> - Selektory, model pudełkowy, flexbox, animacje (<em>CSS Selektory</em>, <em>CSS Model pudełkowy</em>, <em>CSS Flexbox</em>)<br>• <strong>Responsive Design</strong> - Media queries i układy mobile-first<br><br><strong>Jak to działa:</strong><br>1. Przeczytaj zadanie w lewym panelu<br>2. Napisz kod w edytorze<br>3. Zobacz wyniki na żywo w podglądzie<br>4. Otrzymaj natychmiastową informację zwrotną ze wskazówkami<br><br><strong>Skróty klawiszowe:</strong> <kbd>Ctrl+Z</kbd> cofnij, <kbd>Ctrl+Shift+Z</kbd> ponów<br><br><strong>Więcej zasobów:</strong><br>• <a href=\"https://nextlevelshit.github.io/html-over-js/\" target=\"_blank\">HTML over JS</a> - Natywny HTML vs rozwiązania JavaScript<br>• <a href=\"https://nextlevelshit.github.io/web-engineering-mandala/\" target=\"_blank\">Web Engineering Mandala</a> - Mapa technologii JavaScript",
"task": "Write <code>Hello World</code>", "task": "Napisz <code>Hello World</code>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; font-size: 2rem; color: #6366f1; font-weight: bold; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; font-size: 2rem; color: #6366f1; font-weight: bold; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,15 +21,15 @@
{ {
"type": "contains", "type": "contains",
"value": "Hello World", "value": "Hello World",
"message": "Write <code>Hello World</code>" "message": "Napisz <code>Hello World</code>"
} }
] ]
}, },
{ {
"id": "overview", "id": "overview",
"title": "Overview", "title": "Przegląd",
"description": "<strong>You're ready!</strong> Open the menu (☰) to explore all modules.<br><br><strong>Recommended learning path:</strong><br>1. <em>HTML Block & Inline</em> - Understand container vs inline elements<br>2. <em>HTML Forms</em> - Build interactive forms with validation<br>3. <em>CSS Selectors</em> - Target elements precisely<br>4. <em>CSS Box Model</em> - Master padding, margin, borders<br>5. <em>CSS Flexbox</em> - Create flexible layouts<br>6. <em>CSS Animations</em> - Add motion and transitions<br><br><strong>Tips:</strong><br>• Use <em>Show Expected</em> to see the target result<br>• Your progress is saved automatically<br>• Try Emmet in HTML mode: <kbd>ul>li*3</kbd> + Tab<br><br><strong>Open Source:</strong><br>• <a href=\"https://git.librete.ch/public/code-crispies\" target=\"_blank\">Gitea (Source)</a> · <a href=\"https://github.com/nextlevelshit/code-crispies\" target=\"_blank\">GitHub (Mirror)</a><br>• Made by <a href=\"https://librete.ch\" target=\"_blank\">LibreTECH</a> · <a href=\"https://www.linkedin.com/in/michael-werner-czechowski\" target=\"_blank\">Michael Czechowski</a>", "description": "<strong>Jesteś gotowy!</strong> Otwórz menu (☰) aby odkryć wszystkie moduły.<br><br><strong>Zalecana ścieżka nauki:</strong><br>1. <em>HTML Blokowe i Liniowe</em> - Zrozum elementy kontenerowe vs liniowe<br>2. <em>HTML Formularze</em> - Twórz interaktywne formularze z walidacją<br>3. <em>CSS Selektory</em> - Celuj w elementy precyzyjnie<br>4. <em>CSS Model pudełkowy</em> - Opanuj padding, margin, borders<br>5. <em>CSS Flexbox</em> - Twórz elastyczne układy<br>6. <em>CSS Animacje</em> - Dodaj ruch i przejścia<br><br><strong>Wskazówki:</strong><br>• Użyj <em>Pokaż oczekiwane</em> aby zobaczyć docelowy wynik<br>• Twój postęp jest zapisywany automatycznie<br>• Wypróbuj Emmet w trybie HTML: <kbd>ul>li*3</kbd> + Tab<br><br><strong>Open Source:</strong><br>• <a href=\"https://git.librete.ch/public/code-crispies\" target=\"_blank\">Gitea (Source)</a> · <a href=\"https://github.com/nextlevelshit/code-crispies\" target=\"_blank\">GitHub (Mirror)</a><br>• Stworzone przez <a href=\"https://librete.ch\" target=\"_blank\">LibreTECH</a> · <a href=\"https://www.linkedin.com/in/michael-werner-czechowski\" target=\"_blank\">Michael Czechowski</a>",
"task": "Click Next to continue", "task": "Kliknij Dalej aby kontynuować",
"previewHTML": "<p>Hello World! 🌍</p><p>Hallo Welt!</p><p>Bonjour le monde!</p><p>¡Hola Mundo!</p><p>Ciao Mondo!</p><p>Olá Mundo!</p><p>こんにちは世界!</p><p>你好世界!</p><p>안녕 세상!</p><p>Привет мир!</p><p dir=\"rtl\">שלום עולם!</p><p dir=\"rtl\">مرحبا بالعالم!</p>", "previewHTML": "<p>Hello World! 🌍</p><p>Hallo Welt!</p><p>Bonjour le monde!</p><p>¡Hola Mundo!</p><p>Ciao Mondo!</p><p>Olá Mundo!</p><p>こんにちは世界!</p><p>你好世界!</p><p>안녕 세상!</p><p>Привет мир!</p><p dir=\"rtl\">שלום עולם!</p><p dir=\"rtl\">مرحبا بالعالم!</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 12px; } p { margin: 6px 0; padding: 6px 12px; border-radius: 6px; font-weight: 600; font-size: 0.95em; } p:nth-child(1) { background: #fef3c7; color: #92400e; font-size: 1.2em; } p:nth-child(2) { background: #dcfce7; color: #166534; } p:nth-child(3) { background: #dbeafe; color: #1e40af; } p:nth-child(4) { background: #fce7f3; color: #9d174d; } p:nth-child(5) { background: #e0e7ff; color: #4338ca; } p:nth-child(6) { background: #fef9c3; color: #854d0e; } p:nth-child(7) { background: #fee2e2; color: #991b1b; } p:nth-child(8) { background: #f3e8ff; color: #7c3aed; } p:nth-child(9) { background: #ccfbf1; color: #0f766e; } p:nth-child(10) { background: #fae8ff; color: #86198f; } p:nth-child(11) { background: #fef3c7; color: #b45309; } p:nth-child(12) { background: #d1fae5; color: #047857; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 12px; } p { margin: 6px 0; padding: 6px 12px; border-radius: 6px; font-weight: 600; font-size: 0.95em; } p:nth-child(1) { background: #fef3c7; color: #92400e; font-size: 1.2em; } p:nth-child(2) { background: #dcfce7; color: #166534; } p:nth-child(3) { background: #dbeafe; color: #1e40af; } p:nth-child(4) { background: #fce7f3; color: #9d174d; } p:nth-child(5) { background: #e0e7ff; color: #4338ca; } p:nth-child(6) { background: #fef9c3; color: #854d0e; } p:nth-child(7) { background: #fee2e2; color: #991b1b; } p:nth-child(8) { background: #f3e8ff; color: #7c3aed; } p:nth-child(9) { background: #ccfbf1; color: #0f766e; } p:nth-child(10) { background: #fae8ff; color: #86198f; } p:nth-child(11) { background: #fef3c7; color: #b45309; } p:nth-child(12) { background: #d1fae5; color: #047857; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -39,8 +39,8 @@
"validations": [ "validations": [
{ {
"type": "contains", "type": "contains",
"value": "Hello World", "value": "Hello",
"message": "Click Next to continue" "message": "Kliknij Dalej aby kontynuować"
} }
] ]
}, },

View File

@@ -2,18 +2,18 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "box-model", "id": "box-model",
"title": "CSS Box Model", "title": "CSS Box Model",
"description": "Master the fundamental principles of space management in web design through the CSS box model. This module explores how content, padding, borders, and margins combine to create layout structures that are both visually appealing and structurally sound.", "description": "Opanuj podstawowe zasady zarządzania przestrzenią w projektowaniu stron poprzez model pudełkowy CSS. Ten moduł bada, jak treść, padding, ramki i marginesy łączą się, tworząc struktury układu.",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "box-model-1", "id": "box-model-1",
"title": "Box Model Components", "title": "Padding",
"description": "The CSS box model consists of four concentric layers: content area (innermost), padding, border, and margin (outermost). Understanding how these components interact is essential for precise layout control.", "description": "Każdy element w CSS to pudełko z czterema warstwami: treść, padding, ramka i margines. <strong>Padding</strong> tworzy przestrzeń między treścią a krawędzią pudełka.<br><br>Bez paddingu tekst przylega niezręcznie do ramek. Padding sprawia, że treść jest czytelna i wizualnie zbalansowana.<br><br><pre>.card {\n padding: 1rem;\n}</pre>",
"task": "Set <kbd>padding</kbd> to <kbd>1rem</kbd> to create space between the content and border.", "task": "Ta karta profilu wygląda na ciasną. Dodaj <kbd>padding: 1rem</kbd>, aby tekst miał miejsce do oddychania.",
"previewHTML": "<div class=\"box\">Box Model Components</div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: lavender; border: 2px dashed slategray; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".box {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "padding: 1rem;", "solution": "padding: 1rem;",
@@ -22,62 +22,62 @@
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "padding", "expected": "1rem" }, "value": { "property": "padding", "expected": "1rem" },
"message": "Set <kbd>padding: 1rem</kbd>" "message": "Ustaw <kbd>padding: 1rem</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-2", "id": "box-model-2",
"title": "Adding Borders", "title": "Borders",
"description": "Borders outline an element, creating visual separation from surrounding content. The border shorthand accepts three values: width, style, and color.", "description": "Ramki tworzą wizualne granice wokół elementów. Skrót <kbd>border</kbd> przyjmuje trzy wartości: szerokość, styl i kolor.<br><br>Popularne style: <kbd>solid</kbd>, <kbd>dashed</kbd>, <kbd>dotted</kbd>, <kbd>none</kbd>",
"task": "Set <kbd>border</kbd> to <kbd>2px solid darkslategray</kbd>.", "task": "Dodaj subtelny lewy akcent do karty za pomocą <kbd>border-left: 4px solid steelblue</kbd>.",
"previewHTML": "<div class=\"box\">This box needs a border</div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: mintcream; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".box {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "border: 2px solid darkslategray;", "solution": "border-left: 4px solid steelblue;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "regex", "type": "regex",
"value": "border:\\s*2px\\s+solid\\s+darkslategray", "value": "border-left:\\s*4px\\s+solid\\s+steelblue",
"message": "Set <kbd>border: 2px solid darkslategray</kbd>", "message": "Ustaw <kbd>border-left: 4px solid steelblue</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "box-model-3", "id": "box-model-3",
"title": "Adding Margins", "title": "Margins",
"description": "Margins create space between elements, controlling how they relate to one another within a layout. Unlike padding (which affects internal spacing), margins exist outside the element's border.", "description": "Marginesy tworzą przestrzeń <em>na zewnątrz</em> elementu, oddzielając go od sąsiadów. Podczas gdy padding przesuwa treść do wewnątrz, marginesy odpychają inne elementy.",
"task": "Set <kbd>margin</kbd> to <kbd>1rem</kbd> to create space between this element and its neighbors.", "task": "Dodaj przestrzeń między tymi dwiema kartami profilu za pomocą <kbd>margin-bottom: 1rem</kbd> na <kbd>.card</kbd>.",
"previewHTML": "<div class=\"container\"><div class=\"outer\">This box needs margins</div><div class=\"neighbor\">Adjacent element</div></div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article><article class=\"card\"><h3>Alex Rivera</h3><p>UX Designer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .outer { background-color: plum; padding: 1rem; border: 2px solid orchid; } .neighbor { background-color: lightblue; padding: 1rem; border: 2px solid steelblue; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".outer {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "margin: 1rem;", "solution": "margin-bottom: 1rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "margin", "expected": "1rem" }, "value": { "property": "margin-bottom", "expected": "1rem" },
"message": "Set <kbd>margin: 1rem</kbd>" "message": "Ustaw <kbd>margin-bottom: 1rem</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-4", "id": "box-model-4",
"title": "Box Sizing: Border-Box", "title": "Box Sizing",
"description": "The <kbd>box-sizing</kbd> property determines how element dimensions are calculated. The default <kbd>content-box</kbd> excludes padding and border from width/height, while <kbd>border-box</kbd> includes them, making layout calculations more intuitive.", "description": "Domyślnie <kbd>width</kbd> ustawia tylko szerokość treści. Padding i ramki są dodawane. To powoduje problemy z układem.<br><br><kbd>box-sizing: border-box</kbd> włącza padding i ramkę do szerokości, czyniąc rozmiary przewidywalnymi. Większość programistów stosuje to do wszystkich elementów.",
"task": "Set <kbd>box-sizing</kbd> to <kbd>border-box</kbd> so padding and border are included in the width.", "task": "Obie karty mają <kbd>width: 200px</kbd>. Lewa używa domyślnego rozmiaru (content-box), stając się szersza niż oczekiwano. Napraw prawą kartę za pomocą <kbd>box-sizing: border-box</kbd>.",
"previewHTML": "<div class=\"sizing-demo\"><div class=\"box default\">Content-box (default)</div><div class=\"box sized\">Border-box</div></div>", "previewHTML": "<div class=\"demo\"><article class=\"card\">Content-box</article><article class=\"card fix\">Border-box</article></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .sizing-demo { display: flex; gap: 1rem; } .box { width: 200px; padding: 1rem; border: 4px solid teal; background: lightcyan; } .default { box-sizing: content-box; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .demo { display: flex; gap: 1rem; } .card { width: 200px; padding: 1rem; border: 4px solid steelblue; background: white; border-radius: 8px; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".sized {\n ", "codePrefix": ".fix {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "box-sizing: border-box;", "solution": "box-sizing: border-box;",
@@ -86,93 +86,104 @@
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "box-sizing", "expected": "border-box" }, "value": { "property": "box-sizing", "expected": "border-box" },
"message": "Set <kbd>box-sizing: border-box</kbd>" "message": "Ustaw <kbd>box-sizing: border-box</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-5", "id": "box-model-5",
"title": "Margin Collapse", "title": "Padding Shorthand",
"description": "When two vertical margins meet, they collapse to the larger value instead of adding up. Understanding this behavior is crucial for consistent vertical spacing.", "description": "Padding przyjmuje 1-4 wartości:<br>• 1 wartość: wszystkie strony<br>• 2 wartości: pionowo | poziomo<br>• 4 wartości: góra | prawo | dół | lewo",
"task": "Set <kbd>margin-bottom</kbd> to <kbd>2rem</kbd>. Notice the space between paragraphs equals 2rem (not 3rem) due to margin collapse.", "task": "Ten przycisk potrzebuje więcej miejsca poziomego niż pionowego. Ustaw <kbd>padding: 8px 1rem</kbd> (8px góra/dół, 1rem lewo/prawo).",
"previewHTML": "<div class=\"collapse-demo\"><p class=\"first\">This paragraph has a bottom margin.</p><p class=\"second\">This paragraph has a top margin of 1rem.</p></div>", "previewHTML": "<button class=\"btn\">Follow</button>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .collapse-demo { border: 1px solid silver; padding: 8px; background: ghostwhite; } .second { margin-top: 1rem; background: linen; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".first {\n ", "codePrefix": ".btn {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "margin-bottom: 2rem;", "solution": "padding: 8px 1rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "regex",
"value": { "property": "margin-bottom", "expected": "2rem" }, "value": "padding:\\s*8px\\s+1rem",
"message": "Set <kbd>margin-bottom: 2rem</kbd>" "message": "Ustaw <kbd>padding: 8px 1rem</kbd>",
"options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "box-model-6", "id": "box-model-6",
"title": "Margin Shorthand Notation", "title": "Margin Shorthand",
"description": "The margin shorthand can set all four sides at once. Two values set vertical (top/bottom) and horizontal (left/right) margins respectively.", "description": "Margines używa tego samego wzorca skrótu co padding. Typowym wzorcem jest poziome centrowanie elementów blokowych za pomocą <kbd>margin: 0 auto</kbd>.",
"task": "Set <kbd>margin</kbd> to <kbd>1rem 2rem</kbd> for 1rem top/bottom and 2rem left/right.", "task": "Wycentruj tę kartę poziomo. Ustaw <kbd>margin: 0 auto</kbd>, aby automatycznie obliczyć równe marginesy lewo/prawo.",
"previewHTML": "<div class=\"container\"><div class=\"spaced\">This box needs margins: 1rem top/bottom, 2rem left/right</div></div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .spaced { background-color: honeydew; border: 2px solid mediumseagreen; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { width: 250px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".spaced {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "margin: 1rem 2rem;", "solution": "margin: 0 auto;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "regex", "type": "regex",
"value": "margin:\\s*1rem\\s+2rem", "value": "margin:\\s*0\\s+auto",
"message": "Set <kbd>margin: 1rem 2rem</kbd>", "message": "Ustaw <kbd>margin: 0 auto</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "box-model-7", "id": "box-model-7",
"title": "Padding Shorthand Notation", "title": "Border Radius",
"description": "Like margin, padding shorthand allows setting all sides at once. A single value applies to all four sides equally.", "description": "Chociaż nie jest częścią klasycznego modelu pudełkowego, <kbd>border-radius</kbd> zaokrągla rogi ramki elementu. Użyj <kbd>50%</kbd> na kwadratowym elemencie, aby utworzyć koło.",
"task": "Set <kbd>padding</kbd> to <kbd>2rem</kbd> to add equal padding on all sides.", "task": "Zrób okrągły obrazek awatara za pomocą <kbd>border-radius: 50%</kbd>.",
"previewHTML": "<div class=\"padded\">This box needs equal padding on all sides</div>", "previewHTML": "<article class=\"card\"><img class=\"avatar\" src=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='35' r='25' fill='%23666'/%3E%3Ccircle cx='50' cy='90' r='40' fill='%23666'/%3E%3C/svg%3E\" alt=\"Avatar\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .padded { background-color: papayawhip; border: 2px solid orange; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; text-align: center; } .avatar { width: 80px; height: 80px; background: #ddd; margin-bottom: 8px; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".padded {\n ", "codePrefix": ".avatar {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "padding: 2rem;", "solution": "border-radius: 50%;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "padding", "expected": "2rem" }, "value": { "property": "border-radius", "expected": "50%" },
"message": "Set <kbd>padding: 2rem</kbd>" "message": "Ustaw <kbd>border-radius: 50%</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-8", "id": "box-model-8",
"title": "Border on Specific Sides", "title": "Complete Card",
"description": "For granular control, you can target specific sides with <kbd>border-top</kbd>, <kbd>border-right</kbd>, <kbd>border-bottom</kbd>, or <kbd>border-left</kbd>.", "description": "Połączmy wszystko razem. Ta karta powiadomienia potrzebuje stylowania, żeby wyglądać profesjonalnie.",
"task": "Set <kbd>border-bottom</kbd> to <kbd>4px solid dodgerblue</kbd>.", "task": "Ostyluj powiadomienie: dodaj <kbd>padding: 1rem</kbd>, <kbd>border-left: 4px solid coral</kbd> i <kbd>border-radius: 4px</kbd>.",
"previewHTML": "<div class=\"line\">This element needs only a bottom border</div>", "previewHTML": "<div class=\"alert\"><strong>New message</strong><p>You have 3 unread notifications</p></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .line { padding: 1rem; background-color: aliceblue; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { background: seashell; } .alert strong { color: coral; } .alert p { margin: 4px 0 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".line {\n ", "codePrefix": ".alert {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "border-bottom: 4px solid dodgerblue;", "solution": "padding: 1rem;\n border-left: 4px solid coral;\n border-radius: 4px;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{
"type": "property_value",
"value": { "property": "padding", "expected": "1rem" },
"message": "Ustaw <kbd>padding: 1rem</kbd>"
},
{ {
"type": "regex", "type": "regex",
"value": "border-bottom:\\s*4px\\s+solid\\s+dodgerblue", "value": "border-left:\\s*4px\\s+solid\\s+coral",
"message": "Set <kbd>border-bottom: 4px solid dodgerblue</kbd>", "message": "Ustaw <kbd>border-left: 4px solid coral</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "border-radius", "expected": "4px" },
"message": "Ustaw <kbd>border-radius: 4px</kbd>"
} }
] ]
} }

View File

@@ -1,115 +1,100 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "units-variables", "id": "units-variables",
"title": "CSS Units & Variables", "title": "Jednostki CSS i zmienne",
"description": "Understand the variety of CSS measurement units and how to define and use custom properties for maintainable styles.", "description": "Poznaj różnorodność jednostek miar CSS oraz jak definiować i używać właściwości niestandardowych dla łatwych w utrzymaniu stylów.",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "units-1", "id": "units-1",
"title": "Absolute vs. Relative Units", "title": "Relative Units",
"description": "Learn the difference between px, rem, em, %, and vw/vh for flexible, responsive layouts.", "description": "CSS oferuje dwa typy jednostek: <em>absolutne</em> (jak <kbd>px</kbd>) i <em>względne</em> (jak <kbd>%</kbd> i <kbd>rem</kbd>). Jednostki względne dostosowują się do kontekstu, czyniąc layouty elastycznymi i dostępnymi.<br><br><strong>Popularne jednostki względne:</strong><br>• <kbd>%</kbd> Względem elementu nadrzędnego<br>• <kbd>rem</kbd> Względem rozmiaru czcionki root (zwykle 16px)<br>• <kbd>em</kbd> Względem rozmiaru czcionki elementu<br><br>Popularny wzorzec dla czytelnej treści: ustaw <kbd>width: 100%</kbd>, aby wypełnić dostępną przestrzeń, potem <kbd>max-width: 40rem</kbd> aby ograniczyć długość linii dla czytelności.",
"task": "Set the <kbd>width</kbd> of <kbd>.box</kbd> to <kbd>80%</kbd> and <kbd>max-width</kbd> to <kbd>37.5rem</kbd>.", "task": "Ten tekst artykułu jest zbyt szeroki na dużych ekranach. Dodaj <kbd>max-width: 40rem</kbd> dla optymalnej szerokości czytania.",
"previewHTML": "<div class=\"box\">Resize me!</div>", "previewHTML": "<article class=\"article\"><h2>The Art of Typography</h2><p>Good typography is invisible. When text is set well, readers absorb information without noticing the design decisions that make it comfortable to read. Line length is crucial—too wide and eyes get lost, too narrow and reading becomes choppy.</p><p>The ideal line length is 45-75 characters per line. At typical font sizes, this works out to roughly 40rem maximum width.</p></article>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { background: #f5f5f5; padding: 1rem; }", "previewBaseCSS": "body { font-family: Georgia, serif; padding: 1rem; background: #f9f9f9; } .article { background: white; padding: 2rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .article h2 { margin: 0 0 1rem; color: #333; } .article p { margin: 0 0 1rem; line-height: 1.6; color: #444; } .article p:last-child { margin-bottom: 0; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Set flexible sizing */\n.box {", "codePrefix": ".article {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": " width: 80%;\n max-width: 37.5rem;", "solution": "max-width: 40rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ "type": "contains", "value": "width", "message": "Use <kbd>width</kbd> property", "options": { "caseSensitive": false } },
{ "type": "property_value", "value": { "property": "width", "expected": "80%" }, "message": "Set width to <kbd>80%</kbd>" },
{ "type": "contains", "value": "max-width", "message": "Use <kbd>max-width</kbd> property", "options": { "caseSensitive": false } },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "max-width", "expected": "37.5rem" }, "value": { "property": "max-width", "expected": "40rem" },
"message": "Set max-width to <kbd>37.5rem</kbd>" "message": "Ustaw <kbd>max-width: 40rem</kbd>"
} }
] ]
}, },
{ {
"id": "units-2", "id": "units-2",
"title": "CSS Custom Properties", "title": "CSS Variables",
"description": "Define and reuse variables (--custom properties) to centralize your theme values.", "description": "Właściwości niestandardowe CSS (zmienne) pozwalają definiować wartości wielokrotnego użytku. Definiuj je za pomocą <kbd>--nazwa</kbd> i używaj z <kbd>var(--nazwa)</kbd>. Zmienne zdefiniowane na <kbd>:root</kbd> są dostępne wszędzie.",
"task": "Create a <kbd>--main-color</kbd> variable in <kbd>:root</kbd> with <kbd>#6200ee</kbd> and apply it as the border color on <kbd>.themed</kbd>.", "task": "Zdefiniuj <kbd>--brand: steelblue</kbd> w <kbd>:root</kbd>, następnie użyj jako koloru <kbd>background</kbd> dla <kbd>.btn</kbd>.",
"previewHTML": "<div class=\"themed\">Variable Box</div>", "previewHTML": "<div class=\"actions\"><button class=\"btn\">Subscribe</button><button class=\"btn\">Learn More</button></div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .themed { padding: 1rem; border: 0.125rem solid #ddd; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .actions { display: flex; gap: 1rem; } .btn { color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 1rem; cursor: pointer; background: #ccc; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Define and use a CSS variable */\n:root {", "codePrefix": ":root {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}\n.themed { }", "codeSuffix": "\n}\n\n.btn {\n background: var(--brand);\n}",
"solution": " --main-color: #6200ee;\n}\n.themed {\n border-color: var(--main-color);", "solution": "--brand: steelblue;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "contains", "type": "contains",
"value": "--main-color", "value": "--brand",
"message": "Define <kbd>--main-color</kbd> in :root", "message": "Zdefiniuj zmienną <kbd>--brand</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "contains", "type": "contains",
"value": "var(--main-color)", "value": "steelblue",
"message": "Use <kbd>var(--main-color)</kbd>", "message": "Ustaw wartość na <kbd>steelblue</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "border", "expected": "var(--main-color)" },
"message": "Apply variable to border color",
"options": { "exact": false }
} }
] ]
}, },
{ {
"id": "units-3", "id": "units-3",
"title": "Unit Calculations (calc)", "title": "calc() Function",
"description": "Use the <kbd>calc()</kbd> function to combine different units in one expression.", "description": "Funkcja <kbd>calc()</kbd> pozwala mieszać różne jednostki w obliczeniach. Jest niezbędna dla layoutów łączących stałe i elastyczne rozmiary, jak układ z sidebar.",
"task": "Set the <kbd>width</kbd> of <kbd>.sized</kbd> to <kbd>calc(100% - 2rem)</kbd> and <kbd>min-height</kbd> to <kbd>calc(10vh + 1rem)</kbd>.", "task": "Główna treść powinna wypełnić pozostałe miejsce po sidebarze 200px. Ustaw <kbd>width: calc(100% - 200px)</kbd> na <kbd>.main</kbd>.",
"previewHTML": "<div class=\"sized\">Calc Demo</div>", "previewHTML": "<div class=\"layout\"><aside class=\"sidebar\">Sidebar<br>Navigation</aside><main class=\"main\"><h2>Main Content</h2><p>This area should fill the remaining width after accounting for the fixed-width sidebar.</p></main></div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sized { background: #e8f5e9; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .layout { display: flex; gap: 1rem; } .sidebar { width: 200px; background: #1a1a2e; color: white; padding: 1rem; border-radius: 8px; flex-shrink: 0; } .main { background: white; padding: 1rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .main h2 { margin: 0 0 8px; } .main p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Use calc for dynamic sizing */\n.sized {", "codePrefix": ".main {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": " width: calc(100% - 2rem);\n min-height: calc(10vh + 1rem);", "solution": "width: calc(100% - 200px);",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ "type": "contains", "value": "calc", "message": "Use <kbd>calc()</kbd> function", "options": { "caseSensitive": false } },
{ {
"type": "regex", "type": "regex",
"value": "width:\\s*calc\\(100% - 2rem\\)", "value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
"message": "Width should be <kbd>calc(100% - 2rem)</kbd>", "message": "Ustaw <kbd>width: calc(100% - 200px)</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "regex",
"value": "min-height:\\s*calc\\(10vh \\+ 1rem\\)",
"message": "Min-height should be <kbd>calc(10vh + 1rem)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "units-4", "id": "units-4",
"title": "Viewport & Responsive Units", "title": "Viewport Units",
"description": "Control layouts relative to viewport size with vw, vh, and vmin/vmax units.", "description": "Jednostki viewport wymiarują elementy względem okna przeglądarki:<br>• <kbd>vw</kbd> 1% szerokości viewport<br>• <kbd>vh</kbd> 1% wysokości viewport<br><br>Są idealne dla sekcji pełnoekranowych jak banery hero.",
"task": "Give <kbd>.view</kbd> a <kbd>width</kbd> of <kbd>50vw</kbd> and <kbd>height</kbd> of <kbd>20vh</kbd>.", "task": "Spraw, aby ta sekcja hero wypełniła wysokość viewport ustawiając <kbd>min-height: 100vh</kbd>.",
"previewHTML": "<div class=\"view\">Viewport Box</div>", "previewHTML": "<section class=\"hero\"><h1>Welcome</h1><p>Scroll down to explore</p></section>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .view { background: #ffe0b2; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .hero { background: linear-gradient(135deg, #1a1a2e 0%, steelblue 100%); color: white; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 2rem; } .hero h1 { margin: 0 0 1rem; font-size: 2.5rem; } .hero p { margin: 0; opacity: 0.8; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Use viewport units */\n.view {", "codePrefix": ".hero {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": " width: 50vw;\n height: 20vh;", "solution": "min-height: 100vh;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ "type": "contains", "value": "vw", "message": "Use <kbd>vw</kbd> unit", "options": { "caseSensitive": false } }, {
{ "type": "contains", "value": "vh", "message": "Use <kbd>vh</kbd> unit", "options": { "caseSensitive": false } }, "type": "property_value",
{ "type": "property_value", "value": { "property": "width", "expected": "50vw" }, "message": "Set width to <kbd>50vw</kbd>" }, "value": { "property": "min-height", "expected": "100vh" },
{ "type": "property_value", "value": { "property": "height", "expected": "20vh" }, "message": "Set height to <kbd>20vh</kbd>" } "message": "Ustaw <kbd>min-height: 100vh</kbd>"
}
] ]
} }
] ]

View File

@@ -1,15 +1,15 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "transitions-animations", "id": "transitions-animations",
"title": "CSS Animations", "title": "Animacje CSS",
"description": "Bring interactivity to your UI by smoothly transitioning properties and creating keyframe-driven animations.", "description": "Dodaj interaktywność do interfejsu poprzez płynne przejścia właściwości i animacje oparte na keyframes.",
"difficulty": "intermediate", "difficulty": "intermediate",
"lessons": [ "lessons": [
{ {
"id": "transitions-1", "id": "transitions-1",
"title": "Transitions", "title": "Transitions",
"description": "Learn how to apply <kbd>transition</kbd> to properties for smooth changes on state changes.<br><br><pre>transition: property duration;\n/* e.g. transition: background-color 0.3s; */</pre>", "description": "Naucz się jak stosować <kbd>transition</kbd> do właściwości dla płynnych zmian przy zmianie stanu.<br><br><pre>transition: property duration;\n/* np. transition: background-color 0.3s; */</pre>",
"task": "Add <kbd>transition: background-color 0.3s</kbd> to <kbd>.btn</kbd> so the color fades smoothly on hover.", "task": "Dodaj <kbd>transition: background-color 0.3s</kbd>, aby kolor płynnie zmieniał się przy hover.",
"previewHTML": "<button class=\"btn\">Hover Me</button>", "previewHTML": "<button class=\"btn\">Hover Me</button>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: black; color: white; padding: 0.5rem 1rem; border: none; cursor: pointer; } .btn:hover { background: white; color: black; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: black; color: white; padding: 0.5rem 1rem; border: none; cursor: pointer; } .btn:hover { background: white; color: black; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -22,13 +22,13 @@
{ {
"type": "contains", "type": "contains",
"value": "transition", "value": "transition",
"message": "Use the <kbd>transition</kbd> property", "message": "Użyj właściwości <kbd>transition</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "regex", "type": "regex",
"value": "transition:\\s*background-color\\s*0\\.3s", "value": "transition:\\s*background-color\\s*0\\.3s",
"message": "Set <kbd>transition: background-color 0.3s</kbd>", "message": "Ustaw <kbd>transition: background-color 0.3s</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
@@ -36,8 +36,8 @@
{ {
"id": "transitions-2", "id": "transitions-2",
"title": "Timing Funcs", "title": "Timing Funcs",
"description": "Explore easing functions like <kbd>ease</kbd>, <kbd>linear</kbd>, <kbd>ease-in</kbd>, <kbd>ease-out</kbd> to control animation pacing.", "description": "Poznaj funkcje easing jak <kbd>ease</kbd>, <kbd>linear</kbd>, <kbd>ease-in</kbd>, <kbd>ease-out</kbd> do kontrolowania tempa animacji.",
"task": "Set <kbd>transition-timing-function</kbd> to <kbd>ease-in-out</kbd> on <kbd>.btn</kbd>.", "task": "Ustaw <kbd>transition-timing-function</kbd> na <kbd>ease-in-out</kbd>.",
"previewHTML": "<button class=\"btn\">Timing</button>", "previewHTML": "<button class=\"btn\">Timing</button>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: navy; color: gold; padding: 0.5rem 1rem; border: none; cursor: pointer; transition: all 0.5s; } .btn:hover { background: gold; color: navy; transform: scale(1.1); }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: navy; color: gold; padding: 0.5rem 1rem; border: none; cursor: pointer; transition: all 0.5s; } .btn:hover { background: gold; color: navy; transform: scale(1.1); }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -50,21 +50,21 @@
{ {
"type": "contains", "type": "contains",
"value": "transition-timing-function", "value": "transition-timing-function",
"message": "Use <kbd>transition-timing-function</kbd>", "message": "Użyj <kbd>transition-timing-function</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "transition-timing-function", "expected": "ease-in-out" }, "value": { "property": "transition-timing-function", "expected": "ease-in-out" },
"message": "Set timing to <kbd>ease-in-out</kbd>" "message": "Ustaw timing na <kbd>ease-in-out</kbd>"
} }
] ]
}, },
{ {
"id": "transitions-3", "id": "transitions-3",
"title": "Keyframes", "title": "Keyframes",
"description": "Create named animations using <kbd>@keyframes</kbd> and apply them via the <kbd>animation</kbd> shorthand.<br><br><pre>@keyframes bounce {\n 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;\n}</pre>", "description": "Twórz nazwane animacje używając <kbd>@keyframes</kbd> i stosuj je przez skrót <kbd>animation</kbd>.<br><br><pre>@keyframes bounce {\n 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;\n}</pre>",
"task": "Define a keyframe at <kbd>50%</kbd> with <kbd>transform: translateY(-20px)</kbd> and apply <kbd>animation: bounce 1s infinite</kbd> to <kbd>.ball</kbd>.", "task": "Zdefiniuj keyframe przy <kbd>50%</kbd> z <kbd>transform: translateY(-20px)</kbd> i zastosuj <kbd>animation: bounce 1s infinite</kbd> na <kbd>.ball</kbd>.",
"previewHTML": "<div class=\"ball\"></div>", "previewHTML": "<div class=\"ball\"></div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .ball { width: 50px; height: 50px; background: crimson; border-radius: 50%; margin: 2rem auto; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .ball { width: 50px; height: 50px; background: crimson; border-radius: 50%; margin: 2rem auto; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -77,25 +77,25 @@
{ {
"type": "contains", "type": "contains",
"value": "@keyframes bounce", "value": "@keyframes bounce",
"message": "Define <kbd>@keyframes bounce</kbd>", "message": "Zdefiniuj <kbd>@keyframes bounce</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "regex", "type": "regex",
"value": "50%.*transform: translateY\\(-20px\\)", "value": "50%.*transform: translateY\\(-20px\\)",
"message": "At <kbd>50%</kbd>, use <kbd>transform: translateY(-20px)</kbd>", "message": "Przy <kbd>50%</kbd>, użyj <kbd>transform: translateY(-20px)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "contains", "type": "contains",
"value": "animation", "value": "animation",
"message": "Use <kbd>animation</kbd> property on <kbd>.ball</kbd>", "message": "Użyj właściwości <kbd>animation</kbd> na <kbd>.ball</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "regex", "type": "regex",
"value": "animation:.*bounce.*1s.*infinite", "value": "animation:.*bounce.*1s.*infinite",
"message": "Apply <kbd>animation: bounce 1s infinite</kbd>", "message": "Zastosuj <kbd>animation: bounce 1s infinite</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
@@ -103,8 +103,8 @@
{ {
"id": "transitions-4", "id": "transitions-4",
"title": "Animation Properties", "title": "Animation Properties",
"description": "Fine-tune animations with <kbd>animation-delay</kbd>, <kbd>animation-iteration-count</kbd>, <kbd>animation-direction</kbd>, and <kbd>animation-fill-mode</kbd>.", "description": "Dostosuj animacje za pomocą <kbd>animation-delay</kbd>, <kbd>animation-iteration-count</kbd>, <kbd>animation-direction</kbd> i <kbd>animation-fill-mode</kbd>.",
"task": "Apply the <kbd>pulse</kbd> animation to <kbd>.box</kbd> with <kbd>animation-name: pulse</kbd>, <kbd>animation-duration: 2s</kbd>, <kbd>animation-delay: 1s</kbd>, <kbd>animation-iteration-count: 2</kbd>, and <kbd>animation-fill-mode: forwards</kbd>.", "task": "Zastosuj animację <kbd>pulse</kbd> na <kbd>.box</kbd> z <kbd>animation-name: pulse</kbd>, <kbd>animation-duration: 2s</kbd>, <kbd>animation-delay: 1s</kbd>, <kbd>animation-iteration-count: 2</kbd> i <kbd>animation-fill-mode: forwards</kbd>.",
"previewHTML": "<div class=\"box\">Pulse</div>", "previewHTML": "<div class=\"box\">Pulse</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { width: 100px; height: 100px; background: black; color: white; display: flex; align-items: center; justify-content: center; margin: 2rem auto; } @keyframes pulse { 0% { background: black; color: white; transform: scale(1); } 50% { background: white; color: black; transform: scale(1.2); } 100% { background: limegreen; color: black; transform: scale(1); } }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { width: 100px; height: 100px; background: black; color: white; display: flex; align-items: center; justify-content: center; margin: 2rem auto; } @keyframes pulse { 0% { background: black; color: white; transform: scale(1); } 50% { background: white; color: black; transform: scale(1.2); } 100% { background: limegreen; color: black; transform: scale(1); } }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -117,27 +117,27 @@
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-name", "expected": "pulse" }, "value": { "property": "animation-name", "expected": "pulse" },
"message": "Set <kbd>animation-name: pulse</kbd>" "message": "Ustaw <kbd>animation-name: pulse</kbd>"
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-duration", "expected": "2s" }, "value": { "property": "animation-duration", "expected": "2s" },
"message": "Set <kbd>animation-duration: 2s</kbd>" "message": "Ustaw <kbd>animation-duration: 2s</kbd>"
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-delay", "expected": "1s" }, "value": { "property": "animation-delay", "expected": "1s" },
"message": "Set <kbd>animation-delay: 1s</kbd>" "message": "Ustaw <kbd>animation-delay: 1s</kbd>"
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-iteration-count", "expected": "2" }, "value": { "property": "animation-iteration-count", "expected": "2" },
"message": "Set <kbd>animation-iteration-count: 2</kbd>" "message": "Ustaw <kbd>animation-iteration-count: 2</kbd>"
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-fill-mode", "expected": "forwards" }, "value": { "property": "animation-fill-mode", "expected": "forwards" },
"message": "Set <kbd>animation-fill-mode: forwards</kbd>" "message": "Ustaw <kbd>animation-fill-mode: forwards</kbd>"
} }
] ]
} }

View File

@@ -2,14 +2,14 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "responsive-design", "id": "responsive-design",
"title": "CSS Responsive Design", "title": "CSS Responsive Design",
"description": "Make your layouts adapt to different screen sizes using media queries and fluid design techniques.", "description": "Dostosuj swoje layouty do różnych rozmiarów ekranów używając media queries i technik płynnego designu.",
"difficulty": "intermediate", "difficulty": "intermediate",
"lessons": [ "lessons": [
{ {
"id": "responsive-1", "id": "responsive-1",
"title": "Media Queries", "title": "Media Queries",
"description": "Understand the syntax and use cases for CSS media queries to apply styles conditionally based on viewport characteristics.", "description": "Poznaj składnię i przypadki użycia CSS media queries do warunkowego stosowania stylów na podstawie cech viewport.<br><br><pre>@media (max-width: 600px) {\n .panel {\n background: lightcoral;\n }\n}</pre>",
"task": "Write a media query with <kbd>@media (max-width: 600px)</kbd> that changes <kbd>.panel</kbd> background to <kbd>lightcoral</kbd>.", "task": "Napisz media query z <kbd>@media (max-width: 600px)</kbd>, która zmienia tło <kbd>.panel</kbd> na <kbd>lightcoral</kbd>.",
"previewHTML": "<div class=\"panel\">Resize the window</div>", "previewHTML": "<div class=\"panel\">Resize the window</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .panel { padding: 1rem; background: lightblue; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .panel { padding: 1rem; background: lightblue; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -22,19 +22,19 @@
{ {
"type": "regex", "type": "regex",
"value": "@media\\s*\\(max-width:\\s*600px\\)", "value": "@media\\s*\\(max-width:\\s*600px\\)",
"message": "Use <kbd>@media (max-width: 600px)</kbd>", "message": "Użyj <kbd>@media (max-width: 600px)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "contains", "type": "contains",
"value": ".panel", "value": ".panel",
"message": "Target <kbd>.panel</kbd> inside the media query", "message": "Zaadresuj <kbd>.panel</kbd> wewnątrz media query",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "background", "expected": "lightcoral" }, "value": { "property": "background", "expected": "lightcoral" },
"message": "Set <kbd>background: lightcoral</kbd>", "message": "Ustaw <kbd>background: lightcoral</kbd>",
"options": { "exact": false } "options": { "exact": false }
} }
] ]
@@ -42,8 +42,8 @@
{ {
"id": "responsive-2", "id": "responsive-2",
"title": "Fluid Type", "title": "Fluid Type",
"description": "Use relative units like <kbd>vw</kbd> to make font sizes scale with the viewport width.", "description": "Używaj jednostek względnych jak <kbd>vw</kbd>, aby rozmiary czcionek skalowały się z szerokością viewport.",
"task": "Set <kbd>font-size: 5vw</kbd> on <kbd>.text</kbd> so it scales as the viewport changes.", "task": "Ustaw <kbd>font-size: 5vw</kbd>, aby skalowała się ze zmianą viewport.",
"previewHTML": "<p class=\"text\">Fluid Typography</p>", "previewHTML": "<p class=\"text\">Fluid Typography</p>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -53,46 +53,46 @@
"solution": " font-size: 5vw;", "solution": " font-size: 5vw;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "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": "Ustaw <kbd>font-size: 5vw</kbd>" }
] ]
}, },
{ {
"id": "responsive-3", "id": "responsive-3",
"title": "Flex Grids", "title": "Responsive Grid",
"description": "Combine CSS Grid with <kbd>auto-fit</kbd> or <kbd>auto-fill</kbd> for responsive column layouts.", "description": "Połącz CSS Grid z <kbd>auto-fit</kbd> lub <kbd>auto-fill</kbd> dla responsywnych układów kolumnowych, które automatycznie dostosowują liczbę kolumn na podstawie dostępnej przestrzeni.",
"task": "Add <kbd>display: grid</kbd>, <kbd>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</kbd>, and <kbd>gap: 1rem</kbd> to <kbd>.cards</kbd>.", "task": "Dodaj <kbd>display: grid</kbd>, <kbd>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</kbd> i <kbd>gap: 1rem</kbd>.",
"previewHTML": "<div class=\"cards\"><div>1</div><div>2</div><div>3</div><div>4</div></div>", "previewHTML": "<section class=\"features\"><article class=\"feature\"><h3>Fast</h3><p>Lightning quick load times</p></article><article class=\"feature\"><h3>Secure</h3><p>Enterprise-grade security</p></article><article class=\"feature\"><h3>Reliable</h3><p>99.9% uptime guaranteed</p></article><article class=\"feature\"><h3>Support</h3><p>24/7 customer service</p></article></section>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .cards > div { background: #d1c4e9; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .feature { background: white; padding: 1rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .feature h3 { margin: 0 0 8px; color: steelblue; } .feature p { margin: 0; color: #666; font-size: 0.9rem; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Create a responsive grid */\n.cards {", "codePrefix": ".features {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": "display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;", "solution": "display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "display", "expected": "grid" }, "value": { "property": "display", "expected": "grid" },
"message": "Set <kbd>display: grid</kbd>" "message": "Ustaw <kbd>display: grid</kbd>"
}, },
{ {
"type": "regex", "type": "regex",
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)", "value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
"message": "Use <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>", "message": "Użyj <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "gap", "expected": "1rem" }, "value": { "property": "gap", "expected": "1rem" },
"message": "Set <kbd>gap: 1rem</kbd>" "message": "Ustaw <kbd>gap: 1rem</kbd>"
} }
] ]
}, },
{ {
"id": "responsive-4", "id": "responsive-4",
"title": "Mobile-First", "title": "Mobile-First",
"description": "Adopt a mobile-first approach by writing base styles for small screens and enhancing for larger viewports.", "description": "Zastosuj podejście mobile-first: pisz bazowe style dla małych ekranów i rozszerzaj dla większych viewport.",
"task": "Write a media query with <kbd>@media (min-width: 768px)</kbd> that sets <kbd>.sidebar</kbd> width to <kbd>250px</kbd>.", "task": "Napisz media query z <kbd>@media (min-width: 768px)</kbd>, która ustawia szerokość <kbd>.sidebar</kbd> na <kbd>250px</kbd>.",
"previewHTML": "<aside class=\"sidebar\">Sidebar</aside>", "previewHTML": "<aside class=\"sidebar\">Sidebar</aside>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -105,19 +105,19 @@
{ {
"type": "regex", "type": "regex",
"value": "@media\\s*\\(min-width:\\s*768px\\)", "value": "@media\\s*\\(min-width:\\s*768px\\)",
"message": "Use <kbd>@media (min-width: 768px)</kbd>", "message": "Użyj <kbd>@media (min-width: 768px)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "contains", "type": "contains",
"value": ".sidebar", "value": ".sidebar",
"message": "Target <kbd>.sidebar</kbd> inside media query", "message": "Zaadresuj <kbd>.sidebar</kbd> w media query",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "width", "expected": "250px" }, "value": { "property": "width", "expected": "250px" },
"message": "Set <kbd>width: 250px</kbd>", "message": "Ustaw <kbd>width: 250px</kbd>",
"options": { "exact": false } "options": { "exact": false }
} }
] ]

View File

@@ -2,65 +2,65 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-elements", "id": "html-elements",
"title": "HTML Block & Inline", "title": "HTML Block & Inline",
"description": "Understanding the fundamental difference between container (block) and inline elements", "description": "Zrozum podstawową różnicę między elementami kontenerowymi (blokowymi) a elementami liniowymi",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "block-vs-inline-intro", "id": "block-vs-inline-intro",
"title": "Block vs Inline Elements", "title": "Elementy blokowe vs liniowe",
"description": "HTML elements fall into two main categories:<br><br><strong>Block elements</strong> (containers) start on a new line and take full width. Examples: <kbd>&lt;div&gt;</kbd>, <kbd>&lt;p&gt;</kbd>, <kbd>&lt;h1&gt;</kbd>, <kbd>&lt;section&gt;</kbd><br><br><strong>Inline elements</strong> flow within text and only take needed width. Examples: <kbd>&lt;span&gt;</kbd>, <kbd>&lt;a&gt;</kbd>, <kbd>&lt;strong&gt;</kbd>, <kbd>&lt;em&gt;</kbd>", "description": "Elementy HTML dzielą się na dwie główne kategorie:<br><br><strong>Elementy blokowe</strong> (kontenery) zaczynają się w nowej linii i zajmują pełną szerokość. Przykłady: <kbd>&lt;div&gt;</kbd>, <kbd>&lt;p&gt;</kbd>, <kbd>&lt;h1&gt;</kbd>, <kbd>&lt;section&gt;</kbd><br><br><strong>Elementy liniowe</strong> płyną wewnątrz tekstu i zajmują tylko potrzebną szerokość. Przykłady: <kbd>&lt;span&gt;</kbd>, <kbd>&lt;a&gt;</kbd>, <kbd>&lt;strong&gt;</kbd>, <kbd>&lt;em&gt;</kbd>",
"task": "Wrap the word <kbd>important</kbd> with <kbd>&lt;strong&gt;</kbd> tags to make it bold. Notice how the paragraph (block) takes full width while strong (inline) flows with text.", "task": "Otocz słowo <kbd>ważne</kbd> tagami <kbd>&lt;strong&gt;</kbd>, aby je pogrubić. Zauważ, jak akapit (blok) zajmuje pełną szerokość, podczas gdy strong (liniowy) płynie z tekstem.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; } p { background: #e3f2fd; padding: 10px; } strong { background: #ffecb3; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; } p { background: #e3f2fd; padding: 10px; } strong { background: #ffecb3; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<p>This is a paragraph with an important word.</p>", "initialCode": "<p>To jest akapit z ważne słowem.</p>",
"solution": "<p>This is a paragraph with an <strong>important</strong> word.</p>", "solution": "<p>To jest akapit z <strong>ważne</strong> słowem.</p>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "p", "value": "p",
"message": "Add a <kbd>&lt;p&gt;</kbd> paragraph element" "message": "Dodaj element akapitu <kbd>&lt;p&gt;</kbd>"
}, },
{ {
"type": "parent_child", "type": "parent_child",
"value": { "parent": "p", "child": "strong" }, "value": { "parent": "p", "child": "strong" },
"message": "Wrap the word <kbd>important</kbd> with <kbd>&lt;strong&gt;</kbd> tags" "message": "Otocz słowo <kbd>ważne</kbd> tagami <kbd>&lt;strong&gt;</kbd>"
} }
] ]
}, },
{ {
"id": "semantic-containers", "id": "semantic-containers",
"title": "Semantic Tags", "title": "Tagi semantyczne",
"description": "Modern HTML uses semantic containers that describe their content:<br><br><kbd>&lt;header&gt;</kbd> - Page or section header<br><kbd>&lt;nav&gt;</kbd> - Navigation links<br><kbd>&lt;main&gt;</kbd> - Main content area<br><kbd>&lt;section&gt;</kbd> - Thematic grouping<br><kbd>&lt;article&gt;</kbd> - Self-contained content<br><kbd>&lt;footer&gt;</kbd> - Page or section footer", "description": "Nowoczesny HTML używa semantycznych kontenerów, które opisują swoją zawartość:<br><br><kbd>&lt;header&gt;</kbd> - Nagłówek strony lub sekcji<br><kbd>&lt;nav&gt;</kbd> - Linki nawigacyjne<br><kbd>&lt;main&gt;</kbd> - Główna treść<br><kbd>&lt;section&gt;</kbd> - Grupa tematyczna<br><kbd>&lt;article&gt;</kbd> - Samodzielna treść<br><kbd>&lt;footer&gt;</kbd> - Stopka strony lub sekcji",
"task": "Create a basic page structure:<br>1. Add a <kbd>&lt;header&gt;</kbd> with an <kbd>&lt;h1&gt;</kbd> containing the text <code>My Website</code><br>2. Add a <kbd>&lt;main&gt;</kbd> element with a paragraph saying <code>Welcome to my site!</code><br>3. Add a <kbd>&lt;footer&gt;</kbd> with a paragraph saying <code>Copyright 2026</code>", "task": "Stwórz podstawową strukturę strony:<br>1. Dodaj <kbd>&lt;header&gt;</kbd> z <kbd>&lt;h1&gt;</kbd> zawierającym tekst <code>Moja Strona</code><br>2. Dodaj element <kbd>&lt;main&gt;</kbd> z akapitem mówiącym <code>Witaj na mojej stronie!</code><br>3. Dodaj <kbd>&lt;footer&gt;</kbd> z akapitem mówiącym <code>Copyright 2026</code>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; margin: 0; } header { background: #1976d2; color: white; padding: 15px; } main { padding: 20px; min-height: 100px; } footer { background: #424242; color: white; padding: 10px; text-align: center; }", "previewBaseCSS": "body { font-family: system-ui; margin: 0; } header { background: #1976d2; color: white; padding: 15px; } main { padding: 20px; min-height: 100px; } footer { background: #424242; color: white; padding: 10px; text-align: center; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<header>\n <h1>My Website</h1>\n</header>\n<main>\n <p>Welcome to my site!</p>\n</main>\n<footer>\n <p>Copyright 2026</p>\n</footer>", "solution": "<header>\n <h1>Moja Strona</h1>\n</header>\n<main>\n <p>Witaj na mojej stronie!</p>\n</main>\n<footer>\n <p>Copyright 2026</p>\n</footer>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "header", "value": "header",
"message": "Add a <kbd>&lt;header&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;header&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "main", "value": "main",
"message": "Add a <kbd>&lt;main&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;main&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "footer", "value": "footer",
"message": "Add a <kbd>&lt;footer&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;footer&gt;</kbd>"
}, },
{ {
"type": "parent_child", "type": "parent_child",
"value": { "parent": "header", "child": "h1" }, "value": { "parent": "header", "child": "h1" },
"message": "Add an <kbd>&lt;h1&gt;</kbd> heading inside your header" "message": "Dodaj nagłówek <kbd>&lt;h1&gt;</kbd> wewnątrz header"
} }
] ]
} }

View File

@@ -1,100 +1,100 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-basic", "id": "html-forms-basic",
"title": "HTML Forms", "title": "Formularze HTML",
"description": "Learn to create forms with various input types", "description": "Naucz się tworzyć formularze z różnymi typami pól",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "form-structure", "id": "form-structure",
"title": "Form Structure", "title": "Struktura formularza",
"description": "Every form needs a <kbd>&lt;form&gt;</kbd> wrapper. Inside, use <kbd>&lt;label&gt;</kbd> to describe inputs and <kbd>&lt;input&gt;</kbd> for user data entry.<br><br>The <kbd>for</kbd> attribute on labels should match the <kbd>id</kbd> on inputs for accessibility.", "description": "Każdy formularz potrzebuje wrappera <kbd>&lt;form&gt;</kbd>. Wewnątrz używaj <kbd>&lt;label&gt;</kbd> do opisywania pól i <kbd>&lt;input&gt;</kbd> do wprowadzania danych.<br><br>Atrybut <kbd>for</kbd> w labelach powinien odpowiadać <kbd>id</kbd> pól dla dostępności.",
"task": "Create a form with:<br>1. A <kbd>&lt;label&gt;</kbd> with the text <code>Name:</code> and <kbd>for=\"name\"</kbd> attribute<br>2. A text <kbd>&lt;input&gt;</kbd> with <kbd>id=\"name\"</kbd> and <kbd>name=\"name\"</kbd> attributes", "task": "Stwórz formularz z:<br>1. <kbd>&lt;label&gt;</kbd> z tekstem <code>Imię:</code> i atrybutem <kbd>for=\"name\"</kbd><br>2. Tekstowym <kbd>&lt;input&gt;</kbd> z atrybutami <kbd>id=\"name\"</kbd> i <kbd>name=\"name\"</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-bottom: 5px; font-weight: 500; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-bottom: 5px; font-weight: 500; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<form>\n <label for=\"name\">Name:</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n</form>", "solution": "<form>\n <label for=\"name\">Imię:</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n</form>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "form", "value": "form",
"message": "Wrap everything in a <kbd>&lt;form&gt;</kbd> element" "message": "Otocz wszystko elementem <kbd>&lt;form&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "label", "value": "label",
"message": "Add a <kbd>&lt;label&gt;</kbd> for your input" "message": "Dodaj <kbd>&lt;label&gt;</kbd> dla swojego pola"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "input", "value": "input",
"message": "Add an <kbd>&lt;input&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;input&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "label", "attr": "for", "value": null }, "value": { "selector": "label", "attr": "for", "value": null },
"message": "Add a <kbd>for</kbd> attribute to your label" "message": "Dodaj atrybut <kbd>for</kbd> do swojego labela"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input", "attr": "id", "value": null }, "value": { "selector": "input", "attr": "id", "value": null },
"message": "Add an <kbd>id</kbd> attribute to your input" "message": "Dodaj atrybut <kbd>id</kbd> do swojego pola"
} }
] ]
}, },
{ {
"id": "input-types", "id": "input-types",
"title": "Input Types", "title": "Typy pól",
"description": "Different input types provide appropriate keyboards and validation:<br><br><kbd>type=\"text\"</kbd> - General text<br><kbd>type=\"email\"</kbd> - Email with @ validation<br><kbd>type=\"password\"</kbd> - Hidden characters<br><kbd>type=\"number\"</kbd> - Numeric keyboard<br><kbd>type=\"tel\"</kbd> - Phone keyboard", "description": "Różne typy pól zapewniają odpowiednie klawiatury i walidację:<br><br><kbd>type=\"text\"</kbd> - Ogólny tekst<br><kbd>type=\"email\"</kbd> - Email z walidacją @<br><kbd>type=\"password\"</kbd> - Ukryte znaki<br><kbd>type=\"number\"</kbd> - Klawiatura numeryczna<br><kbd>type=\"tel\"</kbd> - Klawiatura telefoniczna",
"task": "Create a login form with two fields:<br>1. An email field: <kbd>&lt;label for=\"email\"&gt;Email:&lt;/label&gt;</kbd> and <kbd>&lt;input type=\"email\" id=\"email\"&gt;</kbd><br>2. A password field: <kbd>&lt;label for=\"password\"&gt;Password:&lt;/label&gt;</kbd> and <kbd>&lt;input type=\"password\" id=\"password\"&gt;</kbd>", "task": "Stwórz formularz logowania z dwoma polami:<br>1. Pole email: <kbd>&lt;label for=\"email\"&gt;Email:&lt;/label&gt;</kbd> i <kbd>&lt;input type=\"email\" id=\"email\"&gt;</kbd><br>2. Pole hasła: <kbd>&lt;label for=\"password\"&gt;Hasło:&lt;/label&gt;</kbd> i <kbd>&lt;input type=\"password\" id=\"password\"&gt;</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<form>\n \n</form>", "initialCode": "<form>\n \n</form>",
"solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\">\n</form>", "solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <label for=\"password\">Hasło:</label>\n <input type=\"password\" id=\"password\" name=\"password\">\n</form>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "input[type='email']", "value": "input[type='email']",
"message": "Add an input with type=\"email\"" "message": "Dodaj pole z type=\"email\""
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "input[type='password']", "value": "input[type='password']",
"message": "Add an input with type=\"password\"" "message": "Dodaj pole z type=\"password\""
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "label", "min": 2 }, "value": { "selector": "label", "min": 2 },
"message": "Add labels for both inputs" "message": "Dodaj labele dla obu pól"
} }
] ]
}, },
{ {
"id": "submit-button", "id": "submit-button",
"title": "Submit Button", "title": "Przycisk wysyłania",
"description": "Forms need a way to submit data. Use:<br><br><kbd>&lt;button type=\"submit\"&gt;</kbd> - Preferred, flexible content<br><kbd>&lt;input type=\"submit\"&gt;</kbd> - Simple text-only button<br><br>The button text should be action-oriented (e.g., <code>Sign In</code>, 'Register', 'Send').", "description": "Formularze potrzebują sposobu na wysłanie danych. Użyj:<br><br><kbd>&lt;button type=\"submit\"&gt;</kbd> - Preferowany, elastyczna zawartość<br><kbd>&lt;input type=\"submit\"&gt;</kbd> - Prosty przycisk tekstowy<br><br>Tekst przycisku powinien być zorientowany na akcję (np. <code>Zaloguj</code>, 'Zarejestruj', 'Wyślij').",
"task": "Add a submit button to the form with the text <code>Sign In</code>.", "task": "Dodaj przycisk wysyłania do formularza z tekstem <code>Zaloguj</code>.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } button { width: 100%; margin-top: 20px; padding: 10px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } button:hover { background: #1565c0; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } button { width: 100%; margin-top: 20px; padding: 10px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } button:hover { background: #1565c0; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n \n</form>", "initialCode": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Hasło:</label>\n <input type=\"password\" id=\"password\">\n \n</form>",
"solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n \n <button type=\"submit\">Sign In</button>\n</form>", "solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Hasło:</label>\n <input type=\"password\" id=\"password\">\n \n <button type=\"submit\">Zaloguj</button>\n</form>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "button[type='submit'], input[type='submit']", "value": "button[type='submit'], input[type='submit']",
"message": "Add a submit button to your form" "message": "Dodaj przycisk wysyłania do formularza"
}, },
{ {
"type": "element_text", "type": "element_text",
"value": { "selector": "button", "text": "Sign In" }, "value": { "selector": "button", "text": "Zaloguj" },
"message": "The button should say <kbd>Sign In</kbd>" "message": "Przycisk powinien wyświetlać <kbd>Zaloguj</kbd>"
} }
] ]
} }

View File

@@ -1,110 +1,32 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-validation", "id": "html-forms-validation",
"title": "HTML Validation", "title": "Walidacja formularzy",
"description": "Learn HTML5 built-in form validation attributes", "description": "Użyj wbudowanej walidacji HTML5 dla lepszego doświadczenia użytkownika",
"mode": "html", "mode": "html",
"difficulty": "intermediate", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "required-fields", "id": "required-fields",
"title": "Required Fields", "title": "Pola wymagane",
"description": "The <kbd>required</kbd> attribute prevents form submission if the field is empty.<br><br>Add it to any input that must be filled:<br><kbd>&lt;input type=\"text\" required&gt;</kbd><br><br>The browser shows a validation message automatically.", "description": "Atrybut <kbd>required</kbd> zapobiega wysłaniu formularza, jeśli pole jest puste. Przeglądarka automatycznie pokazuje komunikat walidacji - bez JavaScript!<br><br>Dodaj go do każdego pola, które musi być wypełnione:<br><kbd>&lt;input type=\"text\" required&gt;</kbd>",
"task": "Make both the name and email fields required by adding the <kbd>required</kbd> attribute.", "task": "Uczyń oba pola (imię i email) wymaganymi, dodając atrybut <kbd>required</kbd> do każdego pola.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-of-type { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid { border-color: #d32f2f; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 320px; } label { display: block; margin-top: 15px; margin-bottom: 5px; font-weight: 500; } label:first-of-type { margin-top: 0; } input { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; } input:focus { outline: 2px solid steelblue; border-color: transparent; } button { margin-top: 20px; padding: 10px 20px; background: steelblue; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 500; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n \n <label for=\"email\">Email: *</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <button type=\"submit\">Submit</button>\n</form>", "initialCode": "<form>\n <label for=\"name\">Imię *</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n \n <label for=\"email\">Email *</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <button type=\"submit\">Wyślij</button>\n</form>",
"solution": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n \n <label for=\"email\">Email: *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <button type=\"submit\">Submit</button>\n</form>", "solution": "<form>\n <label for=\"name\">Imię *</label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n \n <label for=\"email\">Email *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <button type=\"submit\">Wyślij</button>\n</form>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input[name='name']", "attr": "required", "value": true }, "value": { "selector": "input[name='name']", "attr": "required", "value": true },
"message": "Add the <kbd>required</kbd> attribute to the name input" "message": "Dodaj <kbd>required</kbd> do pola imienia"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input[name='email']", "attr": "required", "value": true }, "value": { "selector": "input[name='email']", "attr": "required", "value": true },
"message": "Add the <kbd>required</kbd> attribute to the email input" "message": "Dodaj <kbd>required</kbd> do pola email"
}
]
},
{
"id": "input-constraints",
"title": "Constraints",
"description": "Control what users can enter:<br><br><kbd>minlength</kbd> / <kbd>maxlength</kbd> - Text length limits<br><kbd>min</kbd> / <kbd>max</kbd> - Number range<br><kbd>pattern</kbd> - Regex pattern matching<br><kbd>placeholder</kbd> - Hint text (not a label!)",
"task": "Add validation to the password input:<br>1. Add <kbd>minlength=\"8\"</kbd> for minimum length<br>2. Add <kbd>maxlength=\"20\"</kbd> for maximum length<br>3. Add <kbd>placeholder=\"Enter password\"</kbd> as a hint",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } label { display: block; margin-bottom: 5px; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid:not(:placeholder-shown) { border-color: #d32f2f; } small { display: block; font-size: 12px; color: #666; margin-top: 4px; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }",
"sandboxCSS": "",
"initialCode": "<form>\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Must be 8-20 characters</small>\n \n <button type=\"submit\">Create Account</button>\n</form>",
"solution": "<form>\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\" maxlength=\"20\" placeholder=\"Enter password\" aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Must be 8-20 characters</small>\n \n <button type=\"submit\">Create Account</button>\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "minlength", "value": "8" },
"message": "Add <kbd>minlength</kbd>=\"8\" to the password input"
},
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "maxlength", "value": "20" },
"message": "Add <kbd>maxlength</kbd>=\"20\" to the password input"
},
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "placeholder", "value": null },
"message": "Add a <kbd>placeholder</kbd> to hint what to enter"
}
]
},
{
"id": "complete-registration",
"title": "Full Form",
"description": "Build a complete registration form with all validation concepts:<br><br>- Required fields marked with *<br>- Email validation (use type=\"email\")<br>- Password with length constraints<br>- Terms checkbox (required)<br>- Submit button",
"task": "Complete the registration form. Add required attributes, proper input types, and validation constraints.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 400px; background: #f5f5f5; padding: 25px; border-radius: 8px; } h2 { margin-top: 0; margin-bottom: 20px; } label { display: block; margin-top: 15px; margin-bottom: 5px; font-weight: 500; } input[type='text'], input[type='email'], input[type='password'] { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 14px; } input:focus { outline: 2px solid #1976d2; border-color: transparent; } input[type='checkbox'] { width: auto; margin-right: 8px; vertical-align: middle; } label:has(input[type='checkbox']) { display: flex; align-items: center; font-weight: normal; } button { width: 100%; margin-top: 20px; padding: 12px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 500; } button:hover { background: #1565c0; }",
"sandboxCSS": "",
"initialCode": "<form>\n <h2>Create Account</h2>\n \n <label for=\"fullname\">Full Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\">\n \n <label for=\"email\">Email *</label>\n <input id=\"email\" name=\"email\">\n \n <label for=\"password\">Password *</label>\n <input id=\"password\" name=\"password\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\">\n I agree to the Terms of Service *\n </label>\n \n <button type=\"submit\">Register</button>\n</form>",
"solution": "<form>\n <h2>Create Account</h2>\n \n <label for=\"fullname\">Full Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\" required>\n \n <label for=\"email\">Email *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <label for=\"password\">Password *</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\" required>\n I agree to the Terms of Service *\n </label>\n \n <button type=\"submit\">Register</button>\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "attribute_value",
"value": { "selector": "#fullname", "attr": "required", "value": true },
"message": "Make the full name field <kbd>required</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#email", "attr": "type", "value": "email" },
"message": "Set the email input <kbd>type=\"email\"</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#email", "attr": "required", "value": true },
"message": "Make the email field <kbd>required</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "type", "value": "password" },
"message": "Set the password input <kbd>type=\"password\"</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "required", "value": true },
"message": "Make the password field <kbd>required</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "minlength", "value": "8" },
"message": "Add <kbd>minlength</kbd>=\"8\" to password"
},
{
"type": "attribute_value",
"value": { "selector": "#terms", "attr": "required", "value": true },
"message": "Make the terms checkbox <kbd>required</kbd>"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-details-summary", "id": "html-details-summary",
"title": "HTML Details & Summary", "title": "HTML Details & Summary",
"description": "Create expandable content sections without JavaScript", "description": "Twórz rozwijane sekcje bez JavaScript",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "details-summary-basic", "id": "details-summary-basic",
"title": "First Widget", "title": "Pierwszy widget",
"description": "The <kbd>&lt;details&gt;</kbd> element creates a collapsible section. The <kbd>&lt;summary&gt;</kbd> provides the clickable label.<br><br>Click the summary to toggle the hidden content - no JavaScript needed!", "description": "Element <kbd>&lt;details&gt;</kbd> tworzy zwijany sekcję. Element <kbd>&lt;summary&gt;</kbd> zapewnia klikalną etykietę.<br><br>Kliknij podsumowanie, aby przełączyć ukrytą treść - bez JavaScript!",
"task": "Create a <kbd>&lt;details&gt;</kbd> element with:<br>1. A <kbd>&lt;summary&gt;</kbd> saying <code>Click to reveal</code><br>2. A <kbd>&lt;p&gt;</kbd> with the text <code>This content was hidden!</code>", "task": "Utwórz element <kbd>&lt;details&gt;</kbd> z:<br>1. Elementem <kbd>&lt;summary&gt;</kbd> z tekstem <code>Click to reveal</code><br>2. Elementem <kbd>&lt;p&gt;</kbd> z tekstem <code>This content was hidden!</code>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; } summary { font-weight: 600; cursor: pointer; } summary:hover { color: #1976d2; } details p { margin-top: 15px; color: #666; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; } summary { font-weight: 600; cursor: pointer; } summary:hover { color: #1976d2; } details p { margin-top: 15px; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,30 +21,30 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "details", "value": "details",
"message": "Add a <kbd>&lt;details&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;details&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "summary", "value": "summary",
"message": "Add a <kbd>&lt;summary&gt;</kbd> inside the details" "message": "Dodaj <kbd>&lt;summary&gt;</kbd> wewnątrz details"
}, },
{ {
"type": "parent_child", "type": "parent_child",
"value": { "parent": "details", "child": "summary" }, "value": { "parent": "details", "child": "summary" },
"message": "The <kbd>&lt;summary&gt;</kbd> must be inside <kbd>&lt;details&gt;</kbd>" "message": "Element <kbd>&lt;summary&gt;</kbd> musi być wewnątrz <kbd>&lt;details&gt;</kbd>"
}, },
{ {
"type": "parent_child", "type": "parent_child",
"value": { "parent": "details", "child": "p" }, "value": { "parent": "details", "child": "p" },
"message": "Add a <kbd>&lt;p&gt;</kbd> inside <kbd>&lt;details&gt;</kbd> for the hidden content" "message": "Dodaj <kbd>&lt;p&gt;</kbd> wewnątrz <kbd>&lt;details&gt;</kbd> dla ukrytej treści"
} }
] ]
}, },
{ {
"id": "details-open-attribute", "id": "details-open-attribute",
"title": "Pre-expanded Details", "title": "Domyślnie rozwinięte",
"description": "By default, <kbd>&lt;details&gt;</kbd> is closed. Add the <kbd>open</kbd> attribute to show the content initially.<br><br>This is a boolean attribute - just add <kbd>open</kbd> without a value.", "description": "Domyślnie <kbd>&lt;details&gt;</kbd> jest zamknięte. Dodaj atrybut <kbd>open</kbd>, aby pokazać treść na początku.<br><br>To jest atrybut logiczny - po prostu dodaj <kbd>open</kbd> bez wartości.",
"task": "Add the <kbd>open</kbd> attribute to the <kbd>&lt;details&gt;</kbd> element to show the content by default.", "task": "Dodaj atrybut <kbd>open</kbd> do elementu <kbd>&lt;details&gt;</kbd>, aby domyślnie pokazać treść.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; background: #f9f9f9; } summary { font-weight: 600; cursor: pointer; } details p { margin-top: 15px; color: #666; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; background: #f9f9f9; } summary { font-weight: 600; cursor: pointer; } details p { margin-top: 15px; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -55,15 +55,15 @@
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "details", "attr": "open", "value": true }, "value": { "selector": "details", "attr": "open", "value": true },
"message": "Add the <kbd>open</kbd> attribute to <kbd>&lt;details&gt;</kbd>" "message": "Dodaj atrybut <kbd>open</kbd> do <kbd>&lt;details&gt;</kbd>"
} }
] ]
}, },
{ {
"id": "faq-accordion", "id": "faq-accordion",
"title": "FAQ Accordion", "title": "Akordeon FAQ",
"description": "Multiple <kbd>&lt;details&gt;</kbd> elements create an accordion-style FAQ. Each question can be expanded independently.<br><br><b>Pro tip:</b> Type <kbd>details*3&gt;summary+p</kbd> and press Tab for Emmet expansion. <kbd>*3</kbd> creates 3 elements, <kbd>&gt;</kbd> nests inside, <kbd>+</kbd> adds siblings.", "description": "Wiele elementów <kbd>&lt;details&gt;</kbd> tworzy FAQ w stylu akordeonu. Każde pytanie może być rozwijane niezależnie.<br><br><b>Pro tip:</b> Wpisz <kbd>details*3&gt;summary+p</kbd> i naciśnij Tab dla rozwinięcia Emmet. <kbd>*3</kbd> tworzy 3 elementy, <kbd>&gt;</kbd> zagnieżdża wewnątrz, <kbd>+</kbd> dodaje rodzeństwo.",
"task": "Create an FAQ section with:<br>1. An <kbd>&lt;h1&gt;</kbd> saying <code>Frequently Asked Questions</code><br>2. Three <kbd>&lt;details&gt;</kbd> elements, each with a question in <kbd>&lt;summary&gt;</kbd> and an answer in <kbd>&lt;p&gt;</kbd>", "task": "Utwórz sekcję FAQ z:<br>1. Nagłówkiem <kbd>&lt;h1&gt;</kbd> z tekstem <code>Frequently Asked Questions</code><br>2. Trzema elementami <kbd>&lt;details&gt;</kbd>, każdy z pytaniem w <kbd>&lt;summary&gt;</kbd> i odpowiedzią w <kbd>&lt;p&gt;</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; min-height: 100vh; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); display: flex; flex-direction: column; justify-content: center; padding: 40px; margin: 0; box-sizing: border-box; } h1 { font-size: 2.5rem; color: #4a4a4a; text-align: center; margin: 0 0 30px 0; } details { background: white; border-radius: 12px; margin-bottom: 15px; padding: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } summary { font-size: 1.3rem; font-weight: 600; color: #5c5c5c; cursor: pointer; list-style: none; } summary::before { content: '▸ '; color: #fcb69f; } details[open] summary::before { content: '▾ '; } details p { margin: 15px 0 0 0; color: #666; line-height: 1.6; }", "previewBaseCSS": "body { font-family: system-ui; min-height: 100vh; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); display: flex; flex-direction: column; justify-content: center; padding: 40px; margin: 0; box-sizing: border-box; } h1 { font-size: 2.5rem; color: #4a4a4a; text-align: center; margin: 0 0 30px 0; } details { background: white; border-radius: 12px; margin-bottom: 15px; padding: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } summary { font-size: 1.3rem; font-weight: 600; color: #5c5c5c; cursor: pointer; list-style: none; } summary::before { content: '▸ '; color: #fcb69f; } details[open] summary::before { content: '▾ '; } details p { margin: 15px 0 0 0; color: #666; line-height: 1.6; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -74,22 +74,22 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "h1", "value": "h1",
"message": "Add an <kbd>&lt;h1&gt;</kbd> heading for the FAQ title" "message": "Dodaj nagłówek <kbd>&lt;h1&gt;</kbd> dla tytułu FAQ"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "details", "min": 3 }, "value": { "selector": "details", "min": 3 },
"message": "Create at least 3 <kbd>&lt;details&gt;</kbd> elements for the FAQ" "message": "Utwórz co najmniej 3 elementy <kbd>&lt;details&gt;</kbd> dla FAQ"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "summary", "min": 3 }, "value": { "selector": "summary", "min": 3 },
"message": "Each <kbd>&lt;details&gt;</kbd> needs a <kbd>&lt;summary&gt;</kbd> for the question" "message": "Każdy <kbd>&lt;details&gt;</kbd> potrzebuje <kbd>&lt;summary&gt;</kbd> dla pytania"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "details p", "min": 3 }, "value": { "selector": "details p", "min": 3 },
"message": "Each <kbd>&lt;details&gt;</kbd> needs a <kbd>&lt;p&gt;</kbd> for the answer" "message": "Każdy <kbd>&lt;details&gt;</kbd> potrzebuje <kbd>&lt;p&gt;</kbd> dla odpowiedzi"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-progress-meter", "id": "html-progress-meter",
"title": "HTML Progress & Meter", "title": "HTML Progress & Meter",
"description": "Display completion status and scalar measurements natively", "description": "Wyświetlaj postęp i pomiary natywnie",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "progress-basic", "id": "progress-basic",
"title": "Progress Bars", "title": "Paski postępu",
"description": "The <kbd>&lt;progress&gt;</kbd> element shows task completion. Use <kbd>value</kbd> for current progress and <kbd>max</kbd> for the total.<br><br><b>Note:</b> This is not a self-closing tag! Write <kbd>&lt;progress&gt;...&lt;/progress&gt;</kbd> with fallback text inside for older browsers.", "description": "Element <kbd>&lt;progress&gt;</kbd> pokazuje postęp zadania. Użyj <kbd>value</kbd> dla aktualnego postępu i <kbd>max</kbd> dla całości.<br><br><b>Uwaga:</b> To nie jest samozamykający się tag! Pisz <kbd>&lt;progress&gt;...&lt;/progress&gt;</kbd> z tekstem zastępczym w środku dla starszych przeglądarek.",
"task": "Create a progress bar showing 70% completion:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Download:</code><br>2. Add a <kbd>&lt;progress&gt;</kbd> with <kbd>value=\"70\"</kbd> and <kbd>max=\"100\"</kbd>", "task": "Utwórz pasek postępu pokazujący 70% ukończenia:<br>1. Dodaj <kbd>&lt;label&gt;</kbd> z tekstem <code>Download:</code><br>2. Dodaj <kbd>&lt;progress&gt;</kbd> z <kbd>value=\"70\"</kbd> i <kbd>max=\"100\"</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } progress { width: 100%; height: 20px; border-radius: 10px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 10px; } progress::-webkit-progress-value { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; } progress::-moz-progress-bar { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } progress { width: 100%; height: 20px; border-radius: 10px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 10px; } progress::-webkit-progress-value { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; } progress::-moz-progress-bar { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,30 +21,30 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "progress", "value": "progress",
"message": "Add a <kbd>&lt;progress&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;progress&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "progress", "attr": "value", "value": "70" }, "value": { "selector": "progress", "attr": "value", "value": "70" },
"message": "Set <kbd>value=</kbd>\"70\" on the progress element" "message": "Ustaw <kbd>value=</kbd>\"70\" w elemencie progress"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "progress", "attr": "max", "value": "100" }, "value": { "selector": "progress", "attr": "max", "value": "100" },
"message": "Set <kbd>max=</kbd>\"100\" on the progress element" "message": "Ustaw <kbd>max=</kbd>\"100\" w elemencie progress"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "label", "value": "label",
"message": "Add a <kbd>&lt;label&gt;</kbd> for the progress bar" "message": "Dodaj <kbd>&lt;label&gt;</kbd> dla paska postępu"
} }
] ]
}, },
{ {
"id": "progress-indeterminate", "id": "progress-indeterminate",
"title": "Indeterminate Progress", "title": "Nieokreślony postęp",
"description": "When progress is unknown (like loading), omit the <kbd>value</kbd> attribute. This creates an animated indeterminate state.<br><br>Useful for network requests or processes with unknown duration.", "description": "Gdy postęp jest nieznany (jak przy ładowaniu), pomiń atrybut <kbd>value</kbd>. To tworzy animowany stan nieokreślony.<br><br>Przydatne dla żądań sieciowych lub procesów o nieznanym czasie trwania.",
"task": "Create a loading indicator:<br>1. Add a <kbd>&lt;p&gt;</kbd> saying <code>Loading...</code><br>2. Add a <kbd>&lt;progress&gt;</kbd> without a value attribute", "task": "Utwórz wskaźnik ładowania:<br>1. Dodaj <kbd>&lt;p&gt;</kbd> z tekstem <code>Loading...</code><br>2. Dodaj <kbd>&lt;progress&gt;</kbd> bez atrybutu value",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } p { margin-bottom: 10px; color: #666; } progress { width: 100%; height: 8px; border-radius: 4px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 4px; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } p { margin-bottom: 10px; color: #666; } progress { width: 100%; height: 8px; border-radius: 4px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 4px; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -55,20 +55,20 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "progress", "value": "progress",
"message": "Add a <kbd>&lt;progress&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;progress&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "p", "value": "p",
"message": "Add a <kbd>&lt;p&gt;</kbd> with loading text" "message": "Dodaj <kbd>&lt;p&gt;</kbd> z tekstem ładowania"
} }
] ]
}, },
{ {
"id": "meter-gauge", "id": "meter-gauge",
"title": "Meter Gauges", "title": "Wskaźniki meter",
"description": "The <kbd>&lt;meter&gt;</kbd> element displays a scalar value within a range. Use it for measurements like disk space, battery, or ratings.<br><br>Set <kbd>low</kbd>, <kbd>high</kbd>, and <kbd>optimum</kbd> to define good/bad ranges - the browser colors it accordingly!", "description": "Element <kbd>&lt;meter&gt;</kbd> wyświetla wartość skalarną w zakresie. Używaj go do pomiarów jak przestrzeń dyskowa, bateria lub oceny.<br><br>Ustaw <kbd>low</kbd>, <kbd>high</kbd> i <kbd>optimum</kbd>, aby zdefiniować dobre/złe zakresy - przeglądarka odpowiednio je koloruje!",
"task": "Create a battery level meter:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Battery:</code><br>2. Add a <kbd>&lt;meter&gt;</kbd> with:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> and <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> and <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>", "task": "Utwórz wskaźnik poziomu baterii:<br>1. Dodaj <kbd>&lt;label&gt;</kbd> z tekstem <code>Battery:</code><br>2. Dodaj <kbd>&lt;meter&gt;</kbd> z:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> i <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> i <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } meter { width: 100%; height: 25px; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } meter { width: 100%; height: 25px; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -79,22 +79,42 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "meter", "value": "meter",
"message": "Add a <kbd>&lt;meter&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;meter&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "meter", "attr": "value", "value": "0.8" }, "value": { "selector": "meter", "attr": "value", "value": "0.8" },
"message": "Set <kbd>value=</kbd>\"0.8\" on the meter" "message": "Ustaw <kbd>value=</kbd>\"0.8\" w meter"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "min", "value": "0" },
"message": "Ustaw <kbd>min=</kbd>\"0\" w meter"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "max", "value": "1" },
"message": "Ustaw <kbd>max=</kbd>\"1\" w meter"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "meter", "attr": "low", "value": "0.2" }, "value": { "selector": "meter", "attr": "low", "value": "0.2" },
"message": "Set <kbd>low=</kbd>\"0.2\" to define the low threshold" "message": "Ustaw <kbd>low=</kbd>\"0.2\", aby zdefiniować niski próg"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "high", "value": "0.8" },
"message": "Ustaw <kbd>high=</kbd>\"0.8\", aby zdefiniować wysoki próg"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "optimum", "value": "1" },
"message": "Ustaw <kbd>optimum=</kbd>\"1\", aby wskazać optymalną wartość"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "label", "value": "label",
"message": "Add a <kbd>&lt;label&gt;</kbd> for the meter" "message": "Dodaj <kbd>&lt;label&gt;</kbd> dla meter"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-datalist", "id": "html-datalist",
"title": "Datalist", "title": "Datalist",
"description": "Provide suggestions for text inputs without JavaScript", "description": "Dodaj sugestie do pól tekstowych bez JavaScript",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "datalist-basic", "id": "datalist-basic",
"title": "Input with Suggestions", "title": "Pole z sugestiami",
"description": "The <kbd>&lt;datalist&gt;</kbd> element provides autocomplete suggestions for inputs. Connect it using the <kbd>list</kbd> attribute on the input matching the datalist's <kbd>id</kbd>.<br><br>Users can still type freely - suggestions are just helpers!", "description": "Element <kbd>&lt;datalist&gt;</kbd> dostarcza sugestie autouzupełniania dla pól. Połącz go używając atrybutu <kbd>list</kbd> na polu, pasującego do <kbd>id</kbd> datalist.<br><br>Użytkownicy mogą nadal pisać dowolnie - sugestie to tylko pomocnicy!",
"task": "Create a browser selector:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Browser:</code><br>2. Add an <kbd>&lt;input&gt;</kbd> with <kbd>list=\"browsers\"</kbd><br>3. Add a <kbd>&lt;datalist id=\"browsers\"&gt;</kbd> with options for Chrome, Firefox, and Safari", "task": "Utwórz selektor przeglądarki:<br>1. Dodaj <kbd>&lt;label&gt;</kbd> z tekstem <code>Przeglądarka:</code><br>2. Dodaj <kbd>&lt;input&gt;</kbd> z <kbd>list=\"browsers\"</kbd><br>3. Dodaj <kbd>&lt;datalist id=\"browsers\"&gt;</kbd> z opcjami Chrome, Firefox i Safari",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } input { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; font-size: 16px; } input:focus { outline: 2px solid #1976d2; border-color: transparent; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } input { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; font-size: 16px; } input:focus { outline: 2px solid #1976d2; border-color: transparent; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,30 +21,30 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "datalist", "value": "datalist",
"message": "Add a <kbd>&lt;datalist&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;datalist&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input", "attr": "list", "value": "browsers" }, "value": { "selector": "input", "attr": "list", "value": "browsers" },
"message": "Connect the input to datalist using <kbd>list=</kbd>\"browsers\"" "message": "Połącz pole z datalist używając <kbd>list=</kbd>\"browsers\""
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "option", "min": 3 }, "value": { "selector": "option", "min": 3 },
"message": "Add at least 3 <kbd>&lt;option&gt;</kbd> elements inside <kbd>&lt;datalist&gt;</kbd>" "message": "Dodaj co najmniej 3 elementy <kbd>&lt;option&gt;</kbd> wewnątrz <kbd>&lt;datalist&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "label", "value": "label",
"message": "Add a <kbd>&lt;label&gt;</kbd> for the input" "message": "Dodaj <kbd>&lt;label&gt;</kbd> dla pola"
} }
] ]
}, },
{ {
"id": "datalist-countries", "id": "datalist-countries",
"title": "Country Selector", "title": "Selektor krajów",
"description": "Datalists work great for long lists like countries. Users can type to filter suggestions instantly.<br><br>The <kbd>value</kbd> attribute is what gets entered, and you can add display text after it.", "description": "Datalist świetnie sprawdza się przy długich listach jak kraje. Użytkownicy mogą pisać, aby natychmiast filtrować sugestie.<br><br>Atrybut <kbd>value</kbd> określa, co zostanie wpisane, a po nim możesz dodać tekst wyświetlany.",
"task": "Create a country input:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Country:</code><br>2. Add an <kbd>&lt;input&gt;</kbd> with <kbd>list=\"countries\"</kbd><br>3. Add a <kbd>&lt;datalist id=\"countries\"&gt;</kbd> with at least 4 country options", "task": "Utwórz pole kraju:<br>1. Dodaj <kbd>&lt;label&gt;</kbd> z tekstem <code>Kraj:</code><br>2. Dodaj <kbd>&lt;input&gt;</kbd> z <kbd>list=\"countries\"</kbd><br>3. Dodaj <kbd>&lt;datalist id=\"countries\"&gt;</kbd> z co najmniej 4 opcjami krajów",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 30px; background: linear-gradient(135deg, #e0f7fa 0%, #b2ebf2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } label { display: block; margin-bottom: 10px; font-weight: 600; color: #00695c; } input { width: 100%; padding: 12px 15px; border: 2px solid #26a69a; border-radius: 8px; font-size: 16px; background: white; } input:focus { outline: none; border-color: #00695c; box-shadow: 0 0 0 3px rgba(38,166,154,0.2); }", "previewBaseCSS": "body { font-family: system-ui; padding: 30px; background: linear-gradient(135deg, #e0f7fa 0%, #b2ebf2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } label { display: block; margin-bottom: 10px; font-weight: 600; color: #00695c; } input { width: 100%; padding: 12px 15px; border: 2px solid #26a69a; border-radius: 8px; font-size: 16px; background: white; } input:focus { outline: none; border-color: #00695c; box-shadow: 0 0 0 3px rgba(38,166,154,0.2); }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -55,22 +55,22 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "datalist", "value": "datalist",
"message": "Add a <kbd>&lt;datalist&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;datalist&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "datalist", "attr": "id", "value": "countries" }, "value": { "selector": "datalist", "attr": "id", "value": "countries" },
"message": "Set <kbd>id=</kbd>\"countries\" on the datalist" "message": "Ustaw <kbd>id=</kbd>\"countries\" w datalist"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input", "attr": "list", "value": "countries" }, "value": { "selector": "input", "attr": "list", "value": "countries" },
"message": "Connect the input using <kbd>list=</kbd>\"countries\"" "message": "Połącz pole używając <kbd>list=</kbd>\"countries\""
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "option", "min": 4 }, "value": { "selector": "option", "min": 4 },
"message": "Add at least 4 country options" "message": "Dodaj co najmniej 4 opcje krajów"
} }
] ]
} }

View File

@@ -1,16 +1,16 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-dialog", "id": "html-dialog",
"title": "Dialogs", "title": "Dialogi",
"description": "Create modal dialogs without JavaScript libraries", "description": "Twórz okna dialogowe bez bibliotek JavaScript",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "dialog-basic", "id": "dialog-basic",
"title": "Open Dialog", "title": "Otwórz dialog",
"description": "The <kbd>&lt;dialog&gt;</kbd> element creates a native modal. Add the <kbd>open</kbd> attribute to show it.<br><br>Use <kbd>&lt;form method=\"dialog\"&gt;</kbd> inside to close it when the form submits - no JavaScript needed!", "description": "Element <kbd>&lt;dialog&gt;</kbd> tworzy natywne okno modalne. Dodaj atrybut <kbd>open</kbd>, aby je wyświetlić.<br><br>Użyj <kbd>&lt;form method=\"dialog\"&gt;</kbd> wewnątrz, aby zamknąć je przy wysłaniu formularza - bez JavaScript!",
"task": "Create a dialog with:<br>1. The <kbd>open</kbd> attribute to show it<br>2. An <kbd>&lt;h2&gt;</kbd> saying <code>Welcome!</code><br>3. A <kbd>&lt;p&gt;</kbd> with a greeting message<br>4. A <kbd>&lt;form method=\"dialog\"&gt;</kbd> with a close button", "task": "Utwórz dialog z:<br>1. Atrybutem <kbd>open</kbd> aby go wyświetlić<br>2. Elementem <kbd>&lt;h2&gt;</kbd> z tekstem <code>Witaj!</code><br>3. Elementem <kbd>&lt;p&gt;</kbd> z wiadomością powitalną<br>4. Elementem <kbd>&lt;form method=\"dialog\"&gt;</kbd> z przyciskiem zamknięcia",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; min-height: 200px; background: #f5f5f5; } dialog { border: none; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 25px; max-width: 400px; } dialog::backdrop { background: rgba(0,0,0,0.5); } dialog h2 { margin: 0 0 15px 0; color: #1976d2; } dialog p { color: #666; margin: 0 0 20px 0; } dialog button { background: #1976d2; color: white; border: none; padding: 10px 25px; border-radius: 6px; cursor: pointer; font-size: 16px; } dialog button:hover { background: #1565c0; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; min-height: 200px; background: #f5f5f5; } dialog { border: none; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 25px; max-width: 400px; } dialog::backdrop { background: rgba(0,0,0,0.5); } dialog h2 { margin: 0 0 15px 0; color: #1976d2; } dialog p { color: #666; margin: 0 0 20px 0; } dialog button { background: #1976d2; color: white; border: none; padding: 10px 25px; border-radius: 6px; cursor: pointer; font-size: 16px; } dialog button:hover { background: #1565c0; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,35 +21,35 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog", "value": "dialog",
"message": "Add a <kbd>&lt;dialog&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;dialog&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "dialog", "attr": "open", "value": true }, "value": { "selector": "dialog", "attr": "open", "value": true },
"message": "Add the <kbd>open</kbd> attribute to show the dialog" "message": "Dodaj atrybut <kbd>open</kbd> aby wyświetlić dialog"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog h2", "value": "dialog h2",
"message": "Add an <kbd>&lt;h2&gt;</kbd> heading inside the dialog" "message": "Dodaj nagłówek <kbd>&lt;h2&gt;</kbd> wewnątrz dialogu"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "form[method='dialog']", "value": "form[method='dialog']",
"message": "Add a <kbd>&lt;form method=\"dialog\"&gt;</kbd> for closing" "message": "Dodaj <kbd>&lt;form method=\"dialog\"&gt;</kbd> do zamknięcia"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog button", "value": "dialog button",
"message": "Add a close button inside the form" "message": "Dodaj przycisk zamknięcia wewnątrz formularza"
} }
] ]
}, },
{ {
"id": "dialog-form", "id": "dialog-form",
"title": "Dialog + Form", "title": "Dialog z formularzem",
"description": "Dialogs can contain full forms. The <kbd>method=\"dialog\"</kbd> makes the form close the dialog on submit instead of sending data.<br><br>This pattern is perfect for confirmation dialogs, quick inputs, or settings panels.", "description": "Dialogi mogą zawierać pełne formularze. <kbd>method=\"dialog\"</kbd> sprawia, że formularz zamyka dialog przy wysłaniu zamiast wysyłać dane.<br><br>Ten wzorzec jest idealny dla dialogów potwierdzenia, szybkich wejść lub paneli ustawień.",
"task": "Create a confirmation dialog:<br>1. Add <kbd>open</kbd> to show it<br>2. An <kbd>&lt;h2&gt;</kbd> saying <code>Confirm Delete</code><br>3. A <kbd>&lt;p&gt;</kbd> asking <code>Are you sure?</code><br>4. A <kbd>&lt;form method=\"dialog\"&gt;</kbd> with Cancel and Delete buttons", "task": "Utwórz dialog potwierdzenia:<br>1. Dodaj <kbd>open</kbd> aby go wyświetlić<br>2. Element <kbd>&lt;h2&gt;</kbd> z tekstem <code>Potwierdź usunięcie</code><br>3. Element <kbd>&lt;p&gt;</kbd> z pytaniem <code>Czy jesteś pewien?</code><br>4. Element <kbd>&lt;form method=\"dialog\"&gt;</kbd> z przyciskami Anuluj i Usuń",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; min-height: 200px; background: linear-gradient(135deg, #fce4ec 0%, #f8bbd9 100%); } dialog { border: none; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 25px; max-width: 350px; text-align: center; } dialog h2 { margin: 0 0 10px 0; color: #c62828; } dialog p { color: #666; margin: 0 0 20px 0; } dialog form { display: flex; gap: 10px; justify-content: center; } dialog button { padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; border: none; } dialog button:first-child { background: #e0e0e0; color: #333; } dialog button:last-child { background: #c62828; color: white; } dialog button:hover { opacity: 0.9; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; min-height: 200px; background: linear-gradient(135deg, #fce4ec 0%, #f8bbd9 100%); } dialog { border: none; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 25px; max-width: 350px; text-align: center; } dialog h2 { margin: 0 0 10px 0; color: #c62828; } dialog p { color: #666; margin: 0 0 20px 0; } dialog form { display: flex; gap: 10px; justify-content: center; } dialog button { padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; border: none; } dialog button:first-child { background: #e0e0e0; color: #333; } dialog button:last-child { background: #c62828; color: white; } dialog button:hover { opacity: 0.9; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -60,22 +60,22 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog[open]", "value": "dialog[open]",
"message": "Add a <kbd>&lt;dialog&gt;</kbd> with the open attribute" "message": "Dodaj <kbd>&lt;dialog&gt;</kbd> z atrybutem open"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog h2", "value": "dialog h2",
"message": "Add a heading to the dialog" "message": "Dodaj nagłówek do dialogu"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "form[method='dialog']", "value": "form[method='dialog']",
"message": "Add a <kbd>&lt;form method=\"dialog\"&gt;</kbd> for the buttons" "message": "Dodaj <kbd>&lt;form method=\"dialog\"&gt;</kbd> dla przycisków"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "dialog button", "min": 2 }, "value": { "selector": "dialog button", "min": 2 },
"message": "Add at least 2 buttons (Cancel and Confirm)" "message": "Dodaj co najmniej 2 przyciski (Anuluj i Potwierdź)"
} }
] ]
} }

View File

@@ -1,16 +1,16 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-fieldset", "id": "html-forms-fieldset",
"title": "Fieldsets", "title": "Fieldset",
"description": "Group form controls with fieldset and legend elements", "description": "Grupuj elementy formularza za pomocą fieldset i legend",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "fieldset-basic", "id": "fieldset-basic",
"title": "Grouping with Fieldset", "title": "Grupowanie z Fieldset",
"description": "The <kbd>&lt;fieldset&gt;</kbd> element groups related form controls together. Add a <kbd>&lt;legend&gt;</kbd> as the first child to give the group a title.<br><br>This helps with accessibility and visual organization of complex forms.", "description": "Element <kbd>&lt;fieldset&gt;</kbd> grupuje powiązane elementy formularza. Dodaj <kbd>&lt;legend&gt;</kbd> jako pierwsze dziecko, aby nadać grupie tytuł.<br><br>To pomaga w dostępności i wizualnej organizacji złożonych formularzy.",
"task": "Create a form with a fieldset:<br>1. A <kbd>&lt;form&gt;</kbd> element<br>2. A <kbd>&lt;fieldset&gt;</kbd> inside<br>3. A <kbd>&lt;legend&gt;</kbd> saying <code>Personal Info</code><br>4. Two labeled inputs for name and email", "task": "Utwórz formularz z fieldset:<br>1. Element <kbd>&lt;form&gt;</kbd><br>2. Element <kbd>&lt;fieldset&gt;</kbd> wewnątrz<br>3. Element <kbd>&lt;legend&gt;</kbd> z tekstem <code>Dane osobowe</code><br>4. Dwa opisane pola dla imienia i e-maila",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f0f4f8; } form { max-width: 400px; } fieldset { border: 2px solid #3498db; border-radius: 10px; padding: 20px; background: white; } legend { color: #3498db; font-weight: 600; padding: 0 10px; font-size: 1.1rem; } label { display: block; margin: 15px 0 5px; color: #333; font-weight: 500; } input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; } input:focus { outline: 2px solid #3498db; border-color: transparent; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f0f4f8; } form { max-width: 400px; } fieldset { border: 2px solid #3498db; border-radius: 10px; padding: 20px; background: white; } legend { color: #3498db; font-weight: 600; padding: 0 10px; font-size: 1.1rem; } label { display: block; margin: 15px 0 5px; color: #333; font-weight: 500; } input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; } input:focus { outline: 2px solid #3498db; border-color: transparent; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,35 +21,35 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "form", "value": "form",
"message": "Add a <kbd>&lt;form&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;form&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "fieldset", "value": "fieldset",
"message": "Add a <kbd>&lt;fieldset&gt;</kbd> inside the form" "message": "Dodaj <kbd>&lt;fieldset&gt;</kbd> wewnątrz formularza"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "legend", "value": "legend",
"message": "Add a <kbd>&lt;legend&gt;</kbd> to title your fieldset" "message": "Dodaj <kbd>&lt;legend&gt;</kbd> jako tytuł fieldset"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "label", "min": 2 }, "value": { "selector": "label", "min": 2 },
"message": "Add at least 2 labels" "message": "Dodaj co najmniej 2 etykiety"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "input", "min": 2 }, "value": { "selector": "input", "min": 2 },
"message": "Add at least 2 input fields" "message": "Dodaj co najmniej 2 pola wprowadzania"
} }
] ]
}, },
{ {
"id": "fieldset-textarea", "id": "fieldset-textarea",
"title": "Adding Textarea", "title": "Dodawanie Textarea",
"description": "The <kbd>&lt;textarea&gt;</kbd> element creates a multi-line text input, perfect for longer content like messages or descriptions.<br><br>Use <kbd>rows</kbd> and <kbd>cols</kbd> attributes to set default size.", "description": "Element <kbd>&lt;textarea&gt;</kbd> tworzy wieloliniowe pole tekstowe, idealne dla dłuższych treści jak wiadomości lub opisy.<br><br>Użyj atrybutów <kbd>rows</kbd> i <kbd>cols</kbd> aby ustawić domyślny rozmiar.",
"task": "Create a contact form:<br>1. A <kbd>&lt;fieldset&gt;</kbd> with <kbd>&lt;legend&gt;</kbd> <code>Contact Us</code><br>2. A labeled <kbd>&lt;input&gt;</kbd> for email<br>3. A labeled <kbd>&lt;textarea&gt;</kbd> for the message<br>4. A submit <kbd>&lt;button&gt;</kbd>", "task": "Utwórz formularz kontaktowy:<br>1. Element <kbd>&lt;fieldset&gt;</kbd> z <kbd>&lt;legend&gt;</kbd> <code>Kontakt</code><br>2. Opisane <kbd>&lt;input&gt;</kbd> dla e-maila<br>3. Opisane <kbd>&lt;textarea&gt;</kbd> dla wiadomości<br>4. Przycisk <kbd>&lt;button&gt;</kbd> wysyłania",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } form { max-width: 450px; margin: 0 auto; } fieldset { border: none; border-radius: 15px; padding: 30px; background: white; box-shadow: 0 10px 40px rgba(0,0,0,0.2); } legend { color: #667eea; font-weight: 700; padding: 0; font-size: 1.5rem; margin-bottom: 10px; } label { display: block; margin: 20px 0 8px; color: #333; font-weight: 500; } input, textarea { width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 16px; box-sizing: border-box; font-family: inherit; } input:focus, textarea:focus { outline: none; border-color: #667eea; } textarea { resize: vertical; min-height: 100px; } button { margin-top: 20px; width: 100%; padding: 14px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; } button:hover { opacity: 0.9; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } form { max-width: 450px; margin: 0 auto; } fieldset { border: none; border-radius: 15px; padding: 30px; background: white; box-shadow: 0 10px 40px rgba(0,0,0,0.2); } legend { color: #667eea; font-weight: 700; padding: 0; font-size: 1.5rem; margin-bottom: 10px; } label { display: block; margin: 20px 0 8px; color: #333; font-weight: 500; } input, textarea { width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 16px; box-sizing: border-box; font-family: inherit; } input:focus, textarea:focus { outline: none; border-color: #667eea; } textarea { resize: vertical; min-height: 100px; } button { margin-top: 20px; width: 100%; padding: 14px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; } button:hover { opacity: 0.9; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -60,35 +60,35 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "fieldset", "value": "fieldset",
"message": "Add a <kbd>&lt;fieldset&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;fieldset&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "legend", "value": "legend",
"message": "Add a <kbd>&lt;legend&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;legend&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "textarea", "value": "textarea",
"message": "Add a <kbd>&lt;textarea&gt;</kbd> for the message" "message": "Dodaj <kbd>&lt;textarea&gt;</kbd> dla wiadomości"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "button", "value": "button",
"message": "Add a submit button" "message": "Dodaj przycisk wysyłania"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "input", "value": "input",
"message": "Add an input field for email" "message": "Dodaj pole dla e-maila"
} }
] ]
}, },
{ {
"id": "fieldset-multiple", "id": "fieldset-multiple",
"title": "Multiple Fieldsets", "title": "Wiele Fieldsetów",
"description": "Complex forms can use multiple <kbd>&lt;fieldset&gt;</kbd> elements to organize different sections.<br><br>This improves usability for long forms like registration or checkout pages.", "description": "Złożone formularze mogą używać wielu elementów <kbd>&lt;fieldset&gt;</kbd> do organizacji różnych sekcji.<br><br>To poprawia użyteczność długich formularzy jak rejestracja czy koszyk.",
"task": "Create a registration form with 2 fieldsets:<br>1. <code>Account Info</code> with username and password inputs<br>2. <code>Preferences</code> with a textarea for bio<br>3. A submit button outside the fieldsets", "task": "Utwórz formularz rejestracji z 2 fieldsetami:<br>1. <code>Dane konta</code> z polami nazwa użytkownika i hasło<br>2. <code>Preferencje</code> z textarea dla bio<br>3. Przycisk wysyłania poza fieldsetami",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #fafafa; } form { max-width: 500px; } fieldset { border: 1px solid #ddd; border-radius: 10px; padding: 20px; margin-bottom: 20px; background: white; } legend { color: #2c3e50; font-weight: 600; padding: 0 10px; } label { display: block; margin: 15px 0 5px; color: #555; } input, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; font-family: inherit; } input:focus, textarea:focus { outline: 2px solid #3498db; border-color: transparent; } textarea { resize: vertical; min-height: 80px; } button { width: 100%; padding: 14px; background: #2c3e50; color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; } button:hover { background: #34495e; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #fafafa; } form { max-width: 500px; } fieldset { border: 1px solid #ddd; border-radius: 10px; padding: 20px; margin-bottom: 20px; background: white; } legend { color: #2c3e50; font-weight: 600; padding: 0 10px; } label { display: block; margin: 15px 0 5px; color: #555; } input, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; font-family: inherit; } input:focus, textarea:focus { outline: 2px solid #3498db; border-color: transparent; } textarea { resize: vertical; min-height: 80px; } button { width: 100%; padding: 14px; background: #2c3e50; color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; } button:hover { background: #34495e; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -99,27 +99,27 @@
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "fieldset", "min": 2 }, "value": { "selector": "fieldset", "min": 2 },
"message": "Create at least 2 fieldsets" "message": "Utwórz co najmniej 2 fieldsety"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "legend", "min": 2 }, "value": { "selector": "legend", "min": 2 },
"message": "Add a legend to each fieldset" "message": "Dodaj legend do każdego fieldseta"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "textarea", "value": "textarea",
"message": "Add a textarea for the bio" "message": "Dodaj textarea dla bio"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "button", "value": "button",
"message": "Add a submit button" "message": "Dodaj przycisk wysyłania"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "input", "min": 2 }, "value": { "selector": "input", "min": 2 },
"message": "Add at least 2 input fields" "message": "Dodaj co najmniej 2 pola wprowadzania"
} }
] ]
} }

View File

@@ -1,125 +1,42 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-tables", "id": "html-tables",
"title": "HTML Tables", "title": "Tabele HTML",
"description": "Create structured data tables with headers and captions", "description": "Twórz strukturalne tabele danych z semantycznym znacznikiem",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "table-basic", "id": "table-basic",
"title": "Basic Table Structure", "title": "Tabele danych",
"description": "Tables use <kbd>&lt;table&gt;</kbd> with <kbd>&lt;tr&gt;</kbd> for rows. Inside rows, use <kbd>&lt;th&gt;</kbd> for headers and <kbd>&lt;td&gt;</kbd> for data cells.<br><br>The <kbd>&lt;caption&gt;</kbd> element provides an accessible title for the table.", "description": "Tabele wyświetlają strukturalne dane w wierszach i kolumnach. Użyj <kbd>&lt;table&gt;</kbd> jako kontenera, <kbd>&lt;tr&gt;</kbd> dla wierszy, <kbd>&lt;th&gt;</kbd> dla komórek nagłówka i <kbd>&lt;td&gt;</kbd> dla komórek danych.<br><br>Dodaj <kbd>&lt;caption&gt;</kbd> dla dostępnego tytułu opisującego zawartość tabeli.",
"task": "Create a simple table with:<br>1. A <kbd>&lt;caption&gt;</kbd> saying <code>Fruit Prices</code><br>2. A header row with <code>Fruit</code> and <code>Price</code> columns<br>3. At least 2 data rows", "task": "Utwórz tabelę cenową:<br>1. Element <kbd>&lt;caption&gt;</kbd> z tekstem <code>Pricing</code><br>2. Wiersz nagłówka z <code>Plan</code> i <code>Price</code><br>3. Dwa wiersze danych dla Basic ($9) i Pro ($29)",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px; font-weight: 600; font-size: 1.2rem; color: #333; background: #f8f9fa; } th, td { padding: 12px 20px; text-align: left; border-bottom: 1px solid #eee; } th { background: #3498db; color: white; font-weight: 500; } tr:hover { background: #f8f9fa; } tr:last-child td { border-bottom: none; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; } table { border-collapse: collapse; width: 100%; max-width: 350px; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 16px; font-weight: 600; font-size: 1.1rem; color: #333; background: #f8f9fa; } th, td { padding: 14px 20px; text-align: left; border-bottom: 1px solid #eee; } th { background: steelblue; color: white; font-weight: 500; } tr:last-child td { border-bottom: none; } tr:hover td { background: #f8f9fa; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<table>\n <caption>Fruit Prices</caption>\n <tr>\n <th>Fruit</th>\n <th>Price</th>\n </tr>\n <tr>\n <td>Apple</td>\n <td>$1.50</td>\n </tr>\n <tr>\n <td>Banana</td>\n <td>$0.75</td>\n </tr>\n</table>", "solution": "<table>\n <caption>Pricing</caption>\n <tr>\n <th>Plan</th>\n <th>Price</th>\n </tr>\n <tr>\n <td>Basic</td>\n <td>$9</td>\n </tr>\n <tr>\n <td>Pro</td>\n <td>$29</td>\n </tr>\n</table>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "table", "value": "table",
"message": "Add a <kbd>&lt;table&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;table&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "caption", "value": "caption",
"message": "Add a <kbd>&lt;caption&gt;</kbd> for the table title" "message": "Dodaj <kbd>&lt;caption&gt;</kbd> dla tytułu tabeli"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "th", "min": 2 }, "value": { "selector": "th", "min": 2 },
"message": "Add at least 2 header cells (th)" "message": "Dodaj komórki nagłówka (<kbd>&lt;th&gt;</kbd>) dla Plan i Price"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "tr", "min": 3 }, "value": { "selector": "tr", "min": 3 },
"message": "Add at least 3 rows (1 header + 2 data rows)" "message": "Dodaj 3 wiersze (1 nagłówek + 2 wiersze danych)"
}
]
},
{
"id": "table-thead-tbody",
"title": "Table Head & Body",
"description": "Use <kbd>&lt;thead&gt;</kbd> to group header rows and <kbd>&lt;tbody&gt;</kbd> to group data rows. This helps browsers and assistive technology understand the table structure.<br><br>You can also use <kbd>&lt;tfoot&gt;</kbd> for footer rows like totals.",
"task": "Create a structured table:<br>1. A <kbd>&lt;caption&gt;</kbd> with <code>Monthly Sales</code><br>2. A <kbd>&lt;thead&gt;</kbd> with Month and Revenue headers<br>3. A <kbd>&lt;tbody&gt;</kbd> with at least 2 data rows",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } table { border-collapse: collapse; width: 100%; max-width: 450px; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.2); } caption { padding: 20px; font-weight: 700; font-size: 1.3rem; color: white; background: transparent; text-shadow: 0 2px 4px rgba(0,0,0,0.2); caption-side: top; } thead { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } th { padding: 15px 20px; text-align: left; color: white; font-weight: 500; } tbody tr { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } td { padding: 15px 20px; color: #333; } tbody tr:last-child { border-bottom: none; }",
"sandboxCSS": "",
"initialCode": "",
"solution": "<table>\n <caption>Monthly Sales</caption>\n <thead>\n <tr>\n <th>Month</th>\n <th>Revenue</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>January</td>\n <td>$12,500</td>\n </tr>\n <tr>\n <td>February</td>\n <td>$14,200</td>\n </tr>\n </tbody>\n</table>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "table",
"message": "Add a <kbd>&lt;table&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "caption",
"message": "Add a <kbd>&lt;caption&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "thead",
"message": "Add a <kbd>&lt;thead&gt;</kbd> for the header section"
},
{
"type": "element_exists",
"value": "tbody",
"message": "Add a <kbd>&lt;tbody&gt;</kbd> for the data rows"
},
{
"type": "element_count",
"value": { "selector": "tbody tr", "min": 2 },
"message": "Add at least 2 data rows in tbody"
}
]
},
{
"id": "table-complete",
"title": "Complete Table with Footer",
"description": "Add <kbd>&lt;tfoot&gt;</kbd> to create a footer section for totals or summary data. The footer stays at the bottom even if tbody has many rows.<br><br>Combine all sections for a fully structured, accessible table.",
"task": "Create a complete table:<br>1. A <kbd>&lt;caption&gt;</kbd> with <code>Order Summary</code><br>2. A <kbd>&lt;thead&gt;</kbd> with Item and Price headers<br>3. A <kbd>&lt;tbody&gt;</kbd> with 2 items<br>4. A <kbd>&lt;tfoot&gt;</kbd> with a Total row",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #fafafa; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px 20px; font-weight: 600; font-size: 1.1rem; color: #333; text-align: left; background: #f8f9fa; border-bottom: 2px solid #eee; } th, td { padding: 12px 20px; text-align: left; } thead th { background: #2c3e50; color: white; } tbody td { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } tfoot { background: #ecf0f1; font-weight: 600; } tfoot td { border-top: 2px solid #2c3e50; color: #2c3e50; }",
"sandboxCSS": "",
"initialCode": "",
"solution": "<table>\n <caption>Order Summary</caption>\n <thead>\n <tr>\n <th>Item</th>\n <th>Price</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>Widget</td>\n <td>$25.00</td>\n </tr>\n <tr>\n <td>Gadget</td>\n <td>$35.00</td>\n </tr>\n </tbody>\n <tfoot>\n <tr>\n <td>Total</td>\n <td>$60.00</td>\n </tr>\n </tfoot>\n</table>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "table",
"message": "Add a <kbd>&lt;table&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "caption",
"message": "Add a <kbd>&lt;caption&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "thead",
"message": "Add a <kbd>&lt;thead&gt;</kbd> section"
},
{
"type": "element_exists",
"value": "tbody",
"message": "Add a <kbd>&lt;tbody&gt;</kbd> section"
},
{
"type": "element_exists",
"value": "tfoot",
"message": "Add a <kbd>&lt;tfoot&gt;</kbd> section for the total"
},
{
"type": "element_count",
"value": { "selector": "tbody tr", "min": 2 },
"message": "Add at least 2 item rows in tbody"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-marquee", "id": "html-marquee",
"title": "HTML Marquee", "title": "HTML Marquee",
"description": "Create scrolling text with the classic (deprecated but fun!) marquee element", "description": "Twórz przewijający się tekst za pomocą klasycznego (przestarzałego ale fajnego!) elementu marquee",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "marquee-basic", "id": "marquee-basic",
"title": "Scrolling Text", "title": "Przewijający się tekst",
"description": "The <kbd>&lt;marquee&gt;</kbd> element creates scrolling text - a classic from the early web! While deprecated, it still works in most browsers.<br><br>Note: For production, use CSS animations instead. But for learning and fun, marquee is great!", "description": "Element <kbd>&lt;marquee&gt;</kbd> tworzy przewijający się tekst - klasyk z wczesnego internetu! Choć przestarzały, nadal działa w większości przeglądarek.<br><br>Uwaga: W produkcji używaj animacji CSS. Ale do nauki i zabawy marquee jest świetne!",
"task": "Create a simple marquee:<br>1. Add a <kbd>&lt;marquee&gt;</kbd> element<br>2. Put some text inside like <code>Welcome to my website!</code>", "task": "Utwórz prosty marquee:<br>1. Dodaj element <kbd>&lt;marquee&gt;</kbd><br>2. Umieść w nim tekst jak <code>Witaj na mojej stronie!</code>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2rem; color: #00ff00; text-shadow: 0 0 10px #00ff00, 0 0 20px #00ff00; font-family: 'Courier New', monospace; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2rem; color: #00ff00; text-shadow: 0 0 10px #00ff00, 0 0 20px #00ff00; font-family: 'Courier New', monospace; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,15 +21,15 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "marquee", "value": "marquee",
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;marquee&gt;</kbd>"
} }
] ]
}, },
{ {
"id": "marquee-direction", "id": "marquee-direction",
"title": "Direction & Behavior", "title": "Kierunek i zachowanie",
"description": "Control the marquee with attributes:<br>• <kbd>direction</kbd>: left, right, up, down<br>• <kbd>behavior</kbd>: scroll (default), slide (stops at edge), alternate (bounces)<br>• <kbd>scrollamount</kbd>: speed (default is 6)", "description": "Kontroluj marquee za pomocą atrybutów:<br>• <kbd>direction</kbd>: left, right, up, down<br>• <kbd>behavior</kbd>: scroll (domyślnie), slide (zatrzymuje się na krawędzi), alternate (odbija się)<br>• <kbd>scrollamount</kbd>: prędkość (domyślnie 6)",
"task": "Create a bouncing marquee:<br>1. Add a <kbd>&lt;marquee&gt;</kbd> element<br>2. Set <kbd>behavior=\"alternate\"</kbd> to make it bounce<br>3. Add some fun text", "task": "Utwórz odbijający się marquee:<br>1. Dodaj element <kbd>&lt;marquee&gt;</kbd><br>2. Ustaw <kbd>behavior=\"alternate\"</kbd> aby się odbijał<br>3. Dodaj jakiś fajny tekst",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2.5rem; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); font-weight: bold; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2.5rem; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); font-weight: bold; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -40,20 +40,20 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "marquee", "value": "marquee",
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;marquee&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "marquee", "attr": "behavior", "value": "alternate" }, "value": { "selector": "marquee", "attr": "behavior", "value": "alternate" },
"message": "Add <kbd>behavior=</kbd>\"alternate\" to make it bounce" "message": "Dodaj <kbd>behavior=</kbd>\"alternate\" aby się odbijał"
} }
] ]
}, },
{ {
"id": "marquee-retro", "id": "marquee-retro",
"title": "Retro News Ticker", "title": "Retro pasek wiadomości",
"description": "Combine multiple marquee attributes for a classic news ticker effect. You can even put multiple elements inside!<br><br>Remember: This is deprecated HTML. Modern sites use CSS animations, but marquee is great for understanding web history.", "description": "Połącz wiele atrybutów marquee dla klasycznego efektu paska wiadomości. Możesz nawet umieścić wiele elementów w środku!<br><br>Pamiętaj: To przestarzały HTML. Nowoczesne strony używają animacji CSS, ale marquee jest świetne do zrozumienia historii internetu.",
"task": "Create a news ticker:<br>1. A <kbd>&lt;marquee&gt;</kbd> with <kbd>direction=\"left\"</kbd><br>2. Set <kbd>scrollamount=\"5\"</kbd> for smooth scrolling<br>3. Add a breaking news headline inside", "task": "Utwórz pasek wiadomości:<br>1. Element <kbd>&lt;marquee&gt;</kbd> z <kbd>direction=\"left\"</kbd><br>2. Ustaw <kbd>scrollamount=\"5\"</kbd> dla płynnego przewijania<br>3. Dodaj w środku nagłówek z ostatniej chwili",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 0; margin: 0; background: #1a1a2e; } marquee { background: linear-gradient(90deg, #c0392b 0%, #e74c3c 50%, #c0392b 100%); padding: 15px 0; font-size: 1.3rem; color: white; font-weight: 500; text-transform: uppercase; letter-spacing: 1px; border-top: 3px solid #f1c40f; border-bottom: 3px solid #f1c40f; }", "previewBaseCSS": "body { font-family: system-ui; padding: 0; margin: 0; background: #1a1a2e; } marquee { background: linear-gradient(90deg, #c0392b 0%, #e74c3c 50%, #c0392b 100%); padding: 15px 0; font-size: 1.3rem; color: white; font-weight: 500; text-transform: uppercase; letter-spacing: 1px; border-top: 3px solid #f1c40f; border-bottom: 3px solid #f1c40f; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -64,17 +64,17 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "marquee", "value": "marquee",
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;marquee&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "marquee", "attr": "direction", "value": "left" }, "value": { "selector": "marquee", "attr": "direction", "value": "left" },
"message": "Add <kbd>direction=</kbd>\"left\" for horizontal scrolling" "message": "Dodaj <kbd>direction=</kbd>\"left\" dla poziomego przewijania"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "marquee", "attr": "scrollamount", "value": "5" }, "value": { "selector": "marquee", "attr": "scrollamount", "value": "5" },
"message": "Add <kbd>scrollamount=</kbd>\"5\" for smooth speed" "message": "Dodaj <kbd>scrollamount=</kbd>\"5\" dla płynnej prędkości"
} }
] ]
} }

View File

@@ -2,99 +2,169 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-svg", "id": "html-svg",
"title": "HTML SVG", "title": "HTML SVG",
"description": "Draw scalable vector graphics directly in HTML", "description": "Rysuj skalowalne grafiki wektorowe bezpośrednio w HTML",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "svg-circle", "id": "svg-circle",
"title": "Drawing Circles", "title": "Rysowanie kół",
"description": "SVG (Scalable Vector Graphics) lets you draw shapes directly in HTML. The <kbd>&lt;svg&gt;</kbd> element is the container, with <kbd>width</kbd> and <kbd>height</kbd> attributes.<br><br>Use <kbd>&lt;circle&gt;</kbd> with <kbd>cx</kbd>, <kbd>cy</kbd> (center) and <kbd>r</kbd> (radius) to draw circles.", "description": "SVG (Scalable Vector Graphics) pozwala rysować kształty bezpośrednio w HTML. Element <kbd>&lt;svg&gt;</kbd> jest kontenerem z atrybutami <kbd>width</kbd> i <kbd>height</kbd>.<br><br>Użyj <kbd>&lt;circle&gt;</kbd> z <kbd>cx</kbd>, <kbd>cy</kbd> (środek) i <kbd>r</kbd> (promień) do rysowania kół.",
"task": "Create an SVG with a circle:<br>1. An <kbd>&lt;svg&gt;</kbd> with width=\"200\" and height=\"200\"<br>2. A <kbd>&lt;circle&gt;</kbd> centered at (100,100) with radius 50<br>3. Add a <kbd>fill</kbd> color", "task": "Utwórz SVG z kołem:<br>1. Element <kbd>&lt;svg&gt;</kbd> z width=\"200\" i height=\"200\"<br>2. Element <kbd>&lt;circle&gt;</kbd> wyśrodkowany w (100,100) z promieniem 50<br>3. Dodaj kolor <kbd>fill</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; display: flex; justify-content: center; } svg { background: white; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; display: flex; justify-content: center; } svg { background: white; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"#3498db\" />\n</svg>", "solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"steelblue\" />\n</svg>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "svg", "value": "svg",
"message": "Add an <kbd>&lt;svg&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;svg&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "circle", "value": "circle",
"message": "Add a <kbd>&lt;circle&gt;</kbd> element inside the SVG" "message": "Dodaj element <kbd>&lt;circle&gt;</kbd> wewnątrz SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "width", "value": "200" },
"message": "Ustaw <kbd>width=</kbd>\"200\" w SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "height", "value": "200" },
"message": "Ustaw <kbd>height=</kbd>\"200\" w SVG"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "circle", "attr": "cx", "value": "100" }, "value": { "selector": "circle", "attr": "cx", "value": "100" },
"message": "Set <kbd>cx=</kbd>\"100\" for the circle's horizontal center" "message": "Ustaw <kbd>cx=</kbd>\"100\" dla poziomego środka koła"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "circle", "attr": "cy", "value": "100" }, "value": { "selector": "circle", "attr": "cy", "value": "100" },
"message": "Set <kbd>cy=</kbd>\"100\" for the circle's vertical center" "message": "Ustaw <kbd>cy=</kbd>\"100\" dla pionowego środka koła"
},
{
"type": "attribute_value",
"value": { "selector": "circle", "attr": "r", "value": "50" },
"message": "Ustaw <kbd>r=</kbd>\"50\" dla promienia koła"
} }
] ]
}, },
{ {
"id": "svg-rect-line", "id": "svg-rect-line",
"title": "Rectangles & Lines", "title": "Prostokąty i linie",
"description": "Draw rectangles with <kbd>&lt;rect&gt;</kbd> using <kbd>x</kbd>, <kbd>y</kbd>, <kbd>width</kbd>, <kbd>height</kbd>.<br><br>Draw lines with <kbd>&lt;line&gt;</kbd> using <kbd>x1</kbd>, <kbd>y1</kbd> (start) and <kbd>x2</kbd>, <kbd>y2</kbd> (end). Lines need a <kbd>stroke</kbd> color!", "description": "Rysuj prostokąty za pomocą <kbd>&lt;rect&gt;</kbd> używając <kbd>x</kbd>, <kbd>y</kbd>, <kbd>width</kbd>, <kbd>height</kbd>.<br><br>Rysuj linie za pomocą <kbd>&lt;line&gt;</kbd> używając <kbd>x1</kbd>, <kbd>y1</kbd> (start) i <kbd>x2</kbd>, <kbd>y2</kbd> (koniec). Linie potrzebują koloru <kbd>stroke</kbd>!",
"task": "Create an SVG with:<br>1. An <kbd>&lt;svg&gt;</kbd> (200x150)<br>2. A <kbd>&lt;rect&gt;</kbd> at position (20,20) with size 80x60<br>3. A <kbd>&lt;line&gt;</kbd> from (120,30) to (180,90) with a stroke color", "task": "Utwórz SVG z:<br>1. Elementem <kbd>&lt;svg&gt;</kbd> (200x150)<br>2. Elementem <kbd>&lt;rect&gt;</kbd> na pozycji (20,20) o rozmiarze 80x60<br>3. Elementem <kbd>&lt;line&gt;</kbd> od (120,30) do (180,90) z kolorem stroke",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 200px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 200px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<svg width=\"200\" height=\"150\">\n <rect x=\"20\" y=\"20\" width=\"80\" height=\"60\" fill=\"#e74c3c\" />\n <line x1=\"120\" y1=\"30\" x2=\"180\" y2=\"90\" stroke=\"#2c3e50\" stroke-width=\"3\" />\n</svg>", "solution": "<svg width=\"200\" height=\"150\">\n <rect x=\"20\" y=\"20\" width=\"80\" height=\"60\" fill=\"tomato\" />\n <line x1=\"120\" y1=\"30\" x2=\"180\" y2=\"90\" stroke=\"slategray\" stroke-width=\"3\" />\n</svg>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "svg", "value": "svg",
"message": "Add an <kbd>&lt;svg&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;svg&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "rect", "value": "rect",
"message": "Add a <kbd>&lt;rect&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;rect&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "line", "value": "line",
"message": "Add a <kbd>&lt;line&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;line&gt;</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "width", "value": "200" },
"message": "Ustaw <kbd>width=</kbd>\"200\" w SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "height", "value": "150" },
"message": "Ustaw <kbd>height=</kbd>\"150\" w SVG"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "x", "value": "20" },
"message": "Ustaw <kbd>x=</kbd>\"20\" w rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "y", "value": "20" },
"message": "Ustaw <kbd>y=</kbd>\"20\" w rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "width", "value": "80" },
"message": "Ustaw <kbd>width=</kbd>\"80\" w rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "height", "value": "60" },
"message": "Ustaw <kbd>height=</kbd>\"60\" w rect"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "x1", "value": "120" },
"message": "Ustaw <kbd>x1=</kbd>\"120\" w line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "y1", "value": "30" },
"message": "Ustaw <kbd>y1=</kbd>\"30\" w line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "x2", "value": "180" },
"message": "Ustaw <kbd>x2=</kbd>\"180\" w line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "y2", "value": "90" },
"message": "Ustaw <kbd>y2=</kbd>\"90\" w line"
},
{
"type": "contains",
"value": "stroke",
"message": "Dodaj kolor <kbd>stroke</kbd> do line"
} }
] ]
}, },
{ {
"id": "svg-shapes", "id": "svg-shapes",
"title": "Multiple Shapes", "title": "Wiele kształtów",
"description": "Combine shapes to create simple graphics! Add <kbd>stroke</kbd> for outlines and <kbd>stroke-width</kbd> for thickness.<br><br>Use <kbd>fill=\"none\"</kbd> for hollow shapes. Shapes stack in order - later elements appear on top.", "description": "Łącz kształty, aby tworzyć proste grafiki! Dodaj <kbd>stroke</kbd> dla konturów i <kbd>stroke-width</kbd> dla grubości.<br><br>Użyj <kbd>fill=\"none\"</kbd> dla pustych kształtów. Kształty nakładają się w kolejności - późniejsze elementy są na wierzchu.",
"task": "Create a simple face:<br>1. An <kbd>&lt;svg&gt;</kbd> (200x200)<br>2. A large <kbd>&lt;circle&gt;</kbd> for the face<br>3. Two small <kbd>&lt;circle&gt;</kbd> elements for eyes<br>4. A <kbd>&lt;line&gt;</kbd> for the smile", "task": "Utwórz prostą twarz:<br>1. Element <kbd>&lt;svg&gt;</kbd> (200x200)<br>2. Duże <kbd>&lt;circle&gt;</kbd> dla twarzy<br>3. Dwa małe <kbd>&lt;circle&gt;</kbd> dla oczu<br>4. Element <kbd>&lt;line&gt;</kbd> dla uśmiechu",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); min-height: 250px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); min-height: 250px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"80\" fill=\"#f1c40f\" stroke=\"#e67e22\" stroke-width=\"4\" />\n <circle cx=\"70\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <circle cx=\"130\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <line x1=\"60\" y1=\"130\" x2=\"140\" y2=\"130\" stroke=\"#2c3e50\" stroke-width=\"4\" stroke-linecap=\"round\" />\n</svg>", "solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"80\" fill=\"gold\" stroke=\"orange\" stroke-width=\"4\" />\n <circle cx=\"70\" cy=\"80\" r=\"10\" fill=\"darkslategray\" />\n <circle cx=\"130\" cy=\"80\" r=\"10\" fill=\"darkslategray\" />\n <line x1=\"60\" y1=\"130\" x2=\"140\" y2=\"130\" stroke=\"darkslategray\" stroke-width=\"4\" stroke-linecap=\"round\" />\n</svg>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "svg", "value": "svg",
"message": "Add an <kbd>&lt;svg&gt;</kbd> element" "message": "Dodaj element <kbd>&lt;svg&gt;</kbd>"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "circle", "min": 3 }, "value": { "selector": "circle", "min": 3 },
"message": "Add at least 3 circles (1 face + 2 eyes)" "message": "Dodaj co najmniej 3 koła (1 twarz + 2 oczy)"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "line", "value": "line",
"message": "Add a <kbd>&lt;line&gt;</kbd> for the smile" "message": "Dodaj <kbd>&lt;line&gt;</kbd> dla uśmiechu"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "welcome", "id": "welcome",
"title": "Code Crispies", "title": "Code Crispies",
"description": "Welcome to Code Crispies - your interactive web development learning platform", "description": "Ласкаво просимо до Code Crispies - вашої інтерактивної платформи для вивчення веб-розробки",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "get-started", "id": "get-started",
"title": "Get Started", "title": "Почати",
"description": "<strong>Code Crispies</strong> is a free, open-source platform for learning web development through hands-on exercises. No account required!<br><br><strong>What you'll learn:</strong><br>• <strong>HTML</strong> - Semantic elements, forms, tables, SVG (<em>HTML Block & Inline</em>, <em>HTML Forms</em>, <em>HTML Tables</em>)<br>• <strong>CSS</strong> - Selectors, box model, flexbox, animations (<em>CSS Selectors</em>, <em>CSS Box Model</em>, <em>CSS Flexbox</em>)<br>• <strong>Responsive Design</strong> - Media queries and mobile-first layouts<br><br><strong>How it works:</strong><br>1. Read the task in the left panel<br>2. Write code in the editor<br>3. See live results in the preview<br>4. Get instant feedback with hints<br><br><strong>Keyboard shortcuts:</strong> <kbd>Ctrl+Z</kbd> to undo, <kbd>Ctrl+Shift+Z</kbd> to redo<br><br><strong>More resources:</strong><br>• <a href=\"https://nextlevelshit.github.io/html-over-js/\" target=\"_blank\">HTML over JS</a> - Native HTML vs JavaScript solutions<br>• <a href=\"https://nextlevelshit.github.io/web-engineering-mandala/\" target=\"_blank\">Web Engineering Mandala</a> - JavaScript technology roadmap", "description": "<strong>Code Crispies</strong> - це безкоштовна платформа з відкритим кодом для вивчення веб-розробки через практичні вправи. Обліковий запис не потрібен!<br><br><strong>Що ви вивчите:</strong><br>• <strong>HTML</strong> - Семантичні елементи, форми, таблиці, SVG (<em>HTML Блокові та рядкові</em>, <em>HTML Форми</em>, <em>HTML Таблиці</em>)<br>• <strong>CSS</strong> - Селектори, блокова модель, flexbox, анімації (<em>CSS Селектори</em>, <em>CSS Блокова модель</em>, <em>CSS Flexbox</em>)<br>• <strong>Адаптивний дизайн</strong> - Media queries та mobile-first макети<br><br><strong>Як це працює:</strong><br>1. Прочитайте завдання в лівій панелі<br>2. Напишіть код в редакторі<br>3. Побачте результати в попередньому перегляді<br>4. Отримайте миттєвий зворотний зв'язок з підказками<br><br><strong>Гарячі клавіші:</strong> <kbd>Ctrl+Z</kbd> скасувати, <kbd>Ctrl+Shift+Z</kbd> повторити<br><br><strong>Більше ресурсів:</strong><br>• <a href=\"https://nextlevelshit.github.io/html-over-js/\" target=\"_blank\">HTML over JS</a> - Нативний HTML проти JavaScript рішень<br>• <a href=\"https://nextlevelshit.github.io/web-engineering-mandala/\" target=\"_blank\">Web Engineering Mandala</a> - Карта JavaScript технологій",
"task": "Write <code>Hello World</code>", "task": "Напишіть <code>Hello World</code>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; font-size: 2rem; color: #6366f1; font-weight: bold; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; font-size: 2rem; color: #6366f1; font-weight: bold; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,15 +21,15 @@
{ {
"type": "contains", "type": "contains",
"value": "Hello World", "value": "Hello World",
"message": "Write <code>Hello World</code>" "message": "Напишіть <code>Hello World</code>"
} }
] ]
}, },
{ {
"id": "overview", "id": "overview",
"title": "Overview", "title": "Огляд",
"description": "<strong>You're ready!</strong> Open the menu (☰) to explore all modules.<br><br><strong>Recommended learning path:</strong><br>1. <em>HTML Block & Inline</em> - Understand container vs inline elements<br>2. <em>HTML Forms</em> - Build interactive forms with validation<br>3. <em>CSS Selectors</em> - Target elements precisely<br>4. <em>CSS Box Model</em> - Master padding, margin, borders<br>5. <em>CSS Flexbox</em> - Create flexible layouts<br>6. <em>CSS Animations</em> - Add motion and transitions<br><br><strong>Tips:</strong><br>• Use <em>Show Expected</em> to see the target result<br>• Your progress is saved automatically<br>• Try Emmet in HTML mode: <kbd>ul>li*3</kbd> + Tab<br><br><strong>Open Source:</strong><br>• <a href=\"https://git.librete.ch/public/code-crispies\" target=\"_blank\">Gitea (Source)</a> · <a href=\"https://github.com/nextlevelshit/code-crispies\" target=\"_blank\">GitHub (Mirror)</a><br>• Made by <a href=\"https://librete.ch\" target=\"_blank\">LibreTECH</a> · <a href=\"https://www.linkedin.com/in/michael-werner-czechowski\" target=\"_blank\">Michael Czechowski</a>", "description": "<strong>Ви готові!</strong> Відкрийте меню (☰) щоб дослідити всі модулі.<br><br><strong>Рекомендований шлях навчання:</strong><br>1. <em>HTML Блокові та рядкові</em> - Зрозумійте контейнерні vs рядкові елементи<br>2. <em>HTML Форми</em> - Створюйте інтерактивні форми з валідацією<br>3. <em>CSS Селектори</em> - Точно вибирайте елементи<br>4. <em>CSS Блокова модель</em> - Опануйте padding, margin, borders<br>5. <em>CSS Flexbox</em> - Створюйте гнучкі макети<br>6. <em>CSS Анімації</em> - Додавайте рух та переходи<br><br><strong>Поради:</strong><br>• Використовуйте <em>Показати очікуване</em> щоб побачити цільовий результат<br>• Ваш прогрес зберігається автоматично<br>• Спробуйте Emmet в режимі HTML: <kbd>ul>li*3</kbd> + Tab<br><br><strong>Open Source:</strong><br>• <a href=\"https://git.librete.ch/public/code-crispies\" target=\"_blank\">Gitea (Source)</a> · <a href=\"https://github.com/nextlevelshit/code-crispies\" target=\"_blank\">GitHub (Mirror)</a><br>• Створено <a href=\"https://librete.ch\" target=\"_blank\">LibreTECH</a> · <a href=\"https://www.linkedin.com/in/michael-werner-czechowski\" target=\"_blank\">Michael Czechowski</a>",
"task": "Click Next to continue", "task": "Натисніть Далі щоб продовжити",
"previewHTML": "<p>Hello World! 🌍</p><p>Hallo Welt!</p><p>Bonjour le monde!</p><p>¡Hola Mundo!</p><p>Ciao Mondo!</p><p>Olá Mundo!</p><p>こんにちは世界!</p><p>你好世界!</p><p>안녕 세상!</p><p>Привет мир!</p><p dir=\"rtl\">שלום עולם!</p><p dir=\"rtl\">مرحبا بالعالم!</p>", "previewHTML": "<p>Hello World! 🌍</p><p>Hallo Welt!</p><p>Bonjour le monde!</p><p>¡Hola Mundo!</p><p>Ciao Mondo!</p><p>Olá Mundo!</p><p>こんにちは世界!</p><p>你好世界!</p><p>안녕 세상!</p><p>Привет мир!</p><p dir=\"rtl\">שלום עולם!</p><p dir=\"rtl\">مرحبا بالعالم!</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 12px; } p { margin: 6px 0; padding: 6px 12px; border-radius: 6px; font-weight: 600; font-size: 0.95em; } p:nth-child(1) { background: #fef3c7; color: #92400e; font-size: 1.2em; } p:nth-child(2) { background: #dcfce7; color: #166534; } p:nth-child(3) { background: #dbeafe; color: #1e40af; } p:nth-child(4) { background: #fce7f3; color: #9d174d; } p:nth-child(5) { background: #e0e7ff; color: #4338ca; } p:nth-child(6) { background: #fef9c3; color: #854d0e; } p:nth-child(7) { background: #fee2e2; color: #991b1b; } p:nth-child(8) { background: #f3e8ff; color: #7c3aed; } p:nth-child(9) { background: #ccfbf1; color: #0f766e; } p:nth-child(10) { background: #fae8ff; color: #86198f; } p:nth-child(11) { background: #fef3c7; color: #b45309; } p:nth-child(12) { background: #d1fae5; color: #047857; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 12px; } p { margin: 6px 0; padding: 6px 12px; border-radius: 6px; font-weight: 600; font-size: 0.95em; } p:nth-child(1) { background: #fef3c7; color: #92400e; font-size: 1.2em; } p:nth-child(2) { background: #dcfce7; color: #166534; } p:nth-child(3) { background: #dbeafe; color: #1e40af; } p:nth-child(4) { background: #fce7f3; color: #9d174d; } p:nth-child(5) { background: #e0e7ff; color: #4338ca; } p:nth-child(6) { background: #fef9c3; color: #854d0e; } p:nth-child(7) { background: #fee2e2; color: #991b1b; } p:nth-child(8) { background: #f3e8ff; color: #7c3aed; } p:nth-child(9) { background: #ccfbf1; color: #0f766e; } p:nth-child(10) { background: #fae8ff; color: #86198f; } p:nth-child(11) { background: #fef3c7; color: #b45309; } p:nth-child(12) { background: #d1fae5; color: #047857; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -39,8 +39,8 @@
"validations": [ "validations": [
{ {
"type": "contains", "type": "contains",
"value": "Hello World", "value": "Hello",
"message": "Click Next to continue" "message": "Натисніть Далі щоб продовжити"
} }
] ]
}, },

View File

@@ -2,18 +2,18 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "box-model", "id": "box-model",
"title": "CSS Box Model", "title": "CSS Box Model",
"description": "Master the fundamental principles of space management in web design through the CSS box model. This module explores how content, padding, borders, and margins combine to create layout structures that are both visually appealing and structurally sound.", "description": "Опануйте фундаментальні принципи управління простором у веб-дизайні через блокову модель CSS. Цей модуль досліджує, як контент, відступи, межі та поля поєднуються для створення структур макету.",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "box-model-1", "id": "box-model-1",
"title": "Box Model Components", "title": "Padding",
"description": "The CSS box model consists of four concentric layers: content area (innermost), padding, border, and margin (outermost). Understanding how these components interact is essential for precise layout control.", "description": "Кожен елемент у CSS - це блок з чотирма шарами: контент, відступ (padding), межа та поле. <strong>Padding</strong> створює простір для дихання між вашим контентом і краєм блоку.<br><br>Без padding текст незручно притискається до меж. Padding робить контент читабельним і візуально збалансованим.<br><br><pre>.card {\n padding: 1rem;\n}</pre>",
"task": "Set <kbd>padding</kbd> to <kbd>1rem</kbd> to create space between the content and border.", "task": "Ця картка профілю виглядає тісною. Додайте <kbd>padding: 1rem</kbd>, щоб текст мав простір для дихання.",
"previewHTML": "<div class=\"box\">Box Model Components</div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: lavender; border: 2px dashed slategray; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".box {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "padding: 1rem;", "solution": "padding: 1rem;",
@@ -22,62 +22,62 @@
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "padding", "expected": "1rem" }, "value": { "property": "padding", "expected": "1rem" },
"message": "Set <kbd>padding: 1rem</kbd>" "message": "Встановіть <kbd>padding: 1rem</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-2", "id": "box-model-2",
"title": "Adding Borders", "title": "Borders",
"description": "Borders outline an element, creating visual separation from surrounding content. The border shorthand accepts three values: width, style, and color.", "description": "Межі створюють візуальні границі навколо елементів. Скорочення <kbd>border</kbd> приймає три значення: ширину, стиль і колір.<br><br>Поширені стилі: <kbd>solid</kbd>, <kbd>dashed</kbd>, <kbd>dotted</kbd>, <kbd>none</kbd>",
"task": "Set <kbd>border</kbd> to <kbd>2px solid darkslategray</kbd>.", "task": "Додайте тонкий лівий акцент до картки за допомогою <kbd>border-left: 4px solid steelblue</kbd>.",
"previewHTML": "<div class=\"box\">This box needs a border</div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: mintcream; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".box {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "border: 2px solid darkslategray;", "solution": "border-left: 4px solid steelblue;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "regex", "type": "regex",
"value": "border:\\s*2px\\s+solid\\s+darkslategray", "value": "border-left:\\s*4px\\s+solid\\s+steelblue",
"message": "Set <kbd>border: 2px solid darkslategray</kbd>", "message": "Встановіть <kbd>border-left: 4px solid steelblue</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "box-model-3", "id": "box-model-3",
"title": "Adding Margins", "title": "Margins",
"description": "Margins create space between elements, controlling how they relate to one another within a layout. Unlike padding (which affects internal spacing), margins exist outside the element's border.", "description": "Поля створюють простір <em>зовні</em> елемента, відділяючи його від сусідів. Тоді як padding штовхає контент всередину, поля відштовхують інші елементи.",
"task": "Set <kbd>margin</kbd> to <kbd>1rem</kbd> to create space between this element and its neighbors.", "task": "Додайте простір між цими двома картками профілю за допомогою <kbd>margin-bottom: 1rem</kbd> на <kbd>.card</kbd>.",
"previewHTML": "<div class=\"container\"><div class=\"outer\">This box needs margins</div><div class=\"neighbor\">Adjacent element</div></div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article><article class=\"card\"><h3>Alex Rivera</h3><p>UX Designer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .outer { background-color: plum; padding: 1rem; border: 2px solid orchid; } .neighbor { background-color: lightblue; padding: 1rem; border: 2px solid steelblue; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".outer {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "margin: 1rem;", "solution": "margin-bottom: 1rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "margin", "expected": "1rem" }, "value": { "property": "margin-bottom", "expected": "1rem" },
"message": "Set <kbd>margin: 1rem</kbd>" "message": "Встановіть <kbd>margin-bottom: 1rem</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-4", "id": "box-model-4",
"title": "Box Sizing: Border-Box", "title": "Box Sizing",
"description": "The <kbd>box-sizing</kbd> property determines how element dimensions are calculated. The default <kbd>content-box</kbd> excludes padding and border from width/height, while <kbd>border-box</kbd> includes them, making layout calculations more intuitive.", "description": "За замовчуванням <kbd>width</kbd> встановлює лише ширину контенту. Padding і межі додаються до загальної суми. Це спричиняє проблеми з макетом.<br><br><kbd>box-sizing: border-box</kbd> включає padding і межу у ширину, роблячи розмір передбачуваним. Більшість розробників застосовують це до всіх елементів.",
"task": "Set <kbd>box-sizing</kbd> to <kbd>border-box</kbd> so padding and border are included in the width.", "task": "Обидві картки мають <kbd>width: 200px</kbd>. Ліва використовує стандартний розмір (content-box), стаючи ширшою за очікуване. Виправте праву картку за допомогою <kbd>box-sizing: border-box</kbd>.",
"previewHTML": "<div class=\"sizing-demo\"><div class=\"box default\">Content-box (default)</div><div class=\"box sized\">Border-box</div></div>", "previewHTML": "<div class=\"demo\"><article class=\"card\">Content-box</article><article class=\"card fix\">Border-box</article></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .sizing-demo { display: flex; gap: 1rem; } .box { width: 200px; padding: 1rem; border: 4px solid teal; background: lightcyan; } .default { box-sizing: content-box; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .demo { display: flex; gap: 1rem; } .card { width: 200px; padding: 1rem; border: 4px solid steelblue; background: white; border-radius: 8px; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".sized {\n ", "codePrefix": ".fix {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "box-sizing: border-box;", "solution": "box-sizing: border-box;",
@@ -86,93 +86,104 @@
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "box-sizing", "expected": "border-box" }, "value": { "property": "box-sizing", "expected": "border-box" },
"message": "Set <kbd>box-sizing: border-box</kbd>" "message": "Встановіть <kbd>box-sizing: border-box</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-5", "id": "box-model-5",
"title": "Margin Collapse", "title": "Padding Shorthand",
"description": "When two vertical margins meet, they collapse to the larger value instead of adding up. Understanding this behavior is crucial for consistent vertical spacing.", "description": "Padding приймає 1-4 значення:<br>• 1 значення: всі сторони<br>• 2 значення: вертикально | горизонтально<br>• 4 значення: верх | право | низ | ліво",
"task": "Set <kbd>margin-bottom</kbd> to <kbd>2rem</kbd>. Notice the space between paragraphs equals 2rem (not 3rem) due to margin collapse.", "task": "Ця кнопка потребує більше горизонтального простору, ніж вертикального. Встановіть <kbd>padding: 8px 1rem</kbd> (8px верх/низ, 1rem ліво/право).",
"previewHTML": "<div class=\"collapse-demo\"><p class=\"first\">This paragraph has a bottom margin.</p><p class=\"second\">This paragraph has a top margin of 1rem.</p></div>", "previewHTML": "<button class=\"btn\">Follow</button>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .collapse-demo { border: 1px solid silver; padding: 8px; background: ghostwhite; } .second { margin-top: 1rem; background: linen; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".first {\n ", "codePrefix": ".btn {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "margin-bottom: 2rem;", "solution": "padding: 8px 1rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "regex",
"value": { "property": "margin-bottom", "expected": "2rem" }, "value": "padding:\\s*8px\\s+1rem",
"message": "Set <kbd>margin-bottom: 2rem</kbd>" "message": "Встановіть <kbd>padding: 8px 1rem</kbd>",
"options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "box-model-6", "id": "box-model-6",
"title": "Margin Shorthand Notation", "title": "Margin Shorthand",
"description": "The margin shorthand can set all four sides at once. Two values set vertical (top/bottom) and horizontal (left/right) margins respectively.", "description": "Margin використовує той самий шаблон скорочення, що й padding. Поширений шаблон - горизонтальне центрування блокових елементів за допомогою <kbd>margin: 0 auto</kbd>.",
"task": "Set <kbd>margin</kbd> to <kbd>1rem 2rem</kbd> for 1rem top/bottom and 2rem left/right.", "task": "Відцентруйте цю картку горизонтально. Встановіть <kbd>margin: 0 auto</kbd>, щоб автоматично обчислити рівні ліві/праві поля.",
"previewHTML": "<div class=\"container\"><div class=\"spaced\">This box needs margins: 1rem top/bottom, 2rem left/right</div></div>", "previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .container { background-color: whitesmoke; padding: 8px; } .spaced { background-color: honeydew; border: 2px solid mediumseagreen; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { width: 250px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".spaced {\n ", "codePrefix": ".card {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "margin: 1rem 2rem;", "solution": "margin: 0 auto;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "regex", "type": "regex",
"value": "margin:\\s*1rem\\s+2rem", "value": "margin:\\s*0\\s+auto",
"message": "Set <kbd>margin: 1rem 2rem</kbd>", "message": "Встановіть <kbd>margin: 0 auto</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "box-model-7", "id": "box-model-7",
"title": "Padding Shorthand Notation", "title": "Border Radius",
"description": "Like margin, padding shorthand allows setting all sides at once. A single value applies to all four sides equally.", "description": "Хоча не є частиною класичної блокової моделі, <kbd>border-radius</kbd> заокруглює кути межі елемента. Використовуйте <kbd>50%</kbd> на квадратному елементі, щоб створити коло.",
"task": "Set <kbd>padding</kbd> to <kbd>2rem</kbd> to add equal padding on all sides.", "task": "Зробіть зображення аватара круглим за допомогою <kbd>border-radius: 50%</kbd>.",
"previewHTML": "<div class=\"padded\">This box needs equal padding on all sides</div>", "previewHTML": "<article class=\"card\"><img class=\"avatar\" src=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='35' r='25' fill='%23666'/%3E%3Ccircle cx='50' cy='90' r='40' fill='%23666'/%3E%3C/svg%3E\" alt=\"Avatar\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .padded { background-color: papayawhip; border: 2px solid orange; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; text-align: center; } .avatar { width: 80px; height: 80px; background: #ddd; margin-bottom: 8px; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".padded {\n ", "codePrefix": ".avatar {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "padding: 2rem;", "solution": "border-radius: 50%;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "padding", "expected": "2rem" }, "value": { "property": "border-radius", "expected": "50%" },
"message": "Set <kbd>padding: 2rem</kbd>" "message": "Встановіть <kbd>border-radius: 50%</kbd>"
} }
] ]
}, },
{ {
"id": "box-model-8", "id": "box-model-8",
"title": "Border on Specific Sides", "title": "Complete Card",
"description": "For granular control, you can target specific sides with <kbd>border-top</kbd>, <kbd>border-right</kbd>, <kbd>border-bottom</kbd>, or <kbd>border-left</kbd>.", "description": "Об'єднаймо все разом. Ця картка сповіщення потребує стилізації, щоб виглядати професійно.",
"task": "Set <kbd>border-bottom</kbd> to <kbd>4px solid dodgerblue</kbd>.", "task": "Стилізуйте сповіщення: додайте <kbd>padding: 1rem</kbd>, <kbd>border-left: 4px solid coral</kbd> та <kbd>border-radius: 4px</kbd>.",
"previewHTML": "<div class=\"line\">This element needs only a bottom border</div>", "previewHTML": "<div class=\"alert\"><strong>New message</strong><p>You have 3 unread notifications</p></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .line { padding: 1rem; background-color: aliceblue; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { background: seashell; } .alert strong { color: coral; } .alert p { margin: 4px 0 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": ".line {\n ", "codePrefix": ".alert {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "\n}", "codeSuffix": "\n}",
"solution": "border-bottom: 4px solid dodgerblue;", "solution": "padding: 1rem;\n border-left: 4px solid coral;\n border-radius: 4px;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{
"type": "property_value",
"value": { "property": "padding", "expected": "1rem" },
"message": "Встановіть <kbd>padding: 1rem</kbd>"
},
{ {
"type": "regex", "type": "regex",
"value": "border-bottom:\\s*4px\\s+solid\\s+dodgerblue", "value": "border-left:\\s*4px\\s+solid\\s+coral",
"message": "Set <kbd>border-bottom: 4px solid dodgerblue</kbd>", "message": "Встановіть <kbd>border-left: 4px solid coral</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "border-radius", "expected": "4px" },
"message": "Встановіть <kbd>border-radius: 4px</kbd>"
} }
] ]
} }

View File

@@ -1,115 +1,100 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "units-variables", "id": "units-variables",
"title": "CSS Units & Variables", "title": "Одиниці CSS та змінні",
"description": "Understand the variety of CSS measurement units and how to define and use custom properties for maintainable styles.", "description": "Зрозумійте різноманітність одиниць вимірювання CSS та як визначати й використовувати кастомні властивості для стилів, які легко підтримувати.",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "units-1", "id": "units-1",
"title": "Absolute vs. Relative Units", "title": "Relative Units",
"description": "Learn the difference between px, rem, em, %, and vw/vh for flexible, responsive layouts.", "description": "CSS пропонує два типи одиниць: <em>абсолютні</em> (як <kbd>px</kbd>) та <em>відносні</em> (як <kbd>%</kbd> і <kbd>rem</kbd>). Відносні одиниці адаптуються до контексту, роблячи макети гнучкими та доступними.<br><br><strong>Поширені відносні одиниці:</strong><br>• <kbd>%</kbd> Відносно батьківського елемента<br>• <kbd>rem</kbd> Відносно кореневого розміру шрифту (зазвичай 16px)<br>• <kbd>em</kbd> Відносно розміру шрифту елемента<br><br>Поширений патерн для читабельного контенту: встановіть <kbd>width: 100%</kbd>, щоб заповнити доступний простір, потім <kbd>max-width: 40rem</kbd> для обмеження довжини рядка.",
"task": "Set the <kbd>width</kbd> of <kbd>.box</kbd> to <kbd>80%</kbd> and <kbd>max-width</kbd> to <kbd>37.5rem</kbd>.", "task": "Цей текст статті занадто широкий на великих екранах. Додайте <kbd>max-width: 40rem</kbd> для оптимальної ширини читання.",
"previewHTML": "<div class=\"box\">Resize me!</div>", "previewHTML": "<article class=\"article\"><h2>The Art of Typography</h2><p>Good typography is invisible. When text is set well, readers absorb information without noticing the design decisions that make it comfortable to read. Line length is crucial—too wide and eyes get lost, too narrow and reading becomes choppy.</p><p>The ideal line length is 45-75 characters per line. At typical font sizes, this works out to roughly 40rem maximum width.</p></article>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { background: #f5f5f5; padding: 1rem; }", "previewBaseCSS": "body { font-family: Georgia, serif; padding: 1rem; background: #f9f9f9; } .article { background: white; padding: 2rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .article h2 { margin: 0 0 1rem; color: #333; } .article p { margin: 0 0 1rem; line-height: 1.6; color: #444; } .article p:last-child { margin-bottom: 0; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Set flexible sizing */\n.box {", "codePrefix": ".article {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": " width: 80%;\n max-width: 37.5rem;", "solution": "max-width: 40rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ "type": "contains", "value": "width", "message": "Use <kbd>width</kbd> property", "options": { "caseSensitive": false } },
{ "type": "property_value", "value": { "property": "width", "expected": "80%" }, "message": "Set width to <kbd>80%</kbd>" },
{ "type": "contains", "value": "max-width", "message": "Use <kbd>max-width</kbd> property", "options": { "caseSensitive": false } },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "max-width", "expected": "37.5rem" }, "value": { "property": "max-width", "expected": "40rem" },
"message": "Set max-width to <kbd>37.5rem</kbd>" "message": "Встановіть <kbd>max-width: 40rem</kbd>"
} }
] ]
}, },
{ {
"id": "units-2", "id": "units-2",
"title": "CSS Custom Properties", "title": "CSS Variables",
"description": "Define and reuse variables (--custom properties) to centralize your theme values.", "description": "Кастомні властивості CSS (змінні) дозволяють визначати значення для повторного використання. Визначайте їх за допомогою <kbd>--назва</kbd> та використовуйте з <kbd>var(--назва)</kbd>. Змінні, визначені на <kbd>:root</kbd>, доступні всюди.",
"task": "Create a <kbd>--main-color</kbd> variable in <kbd>:root</kbd> with <kbd>#6200ee</kbd> and apply it as the border color on <kbd>.themed</kbd>.", "task": "Визначте <kbd>--brand: steelblue</kbd> в <kbd>:root</kbd>, потім використайте як колір <kbd>background</kbd> для <kbd>.btn</kbd>.",
"previewHTML": "<div class=\"themed\">Variable Box</div>", "previewHTML": "<div class=\"actions\"><button class=\"btn\">Subscribe</button><button class=\"btn\">Learn More</button></div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .themed { padding: 1rem; border: 0.125rem solid #ddd; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .actions { display: flex; gap: 1rem; } .btn { color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 1rem; cursor: pointer; background: #ccc; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Define and use a CSS variable */\n:root {", "codePrefix": ":root {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}\n.themed { }", "codeSuffix": "\n}\n\n.btn {\n background: var(--brand);\n}",
"solution": " --main-color: #6200ee;\n}\n.themed {\n border-color: var(--main-color);", "solution": "--brand: steelblue;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "contains", "type": "contains",
"value": "--main-color", "value": "--brand",
"message": "Define <kbd>--main-color</kbd> in :root", "message": "Визначте змінну <kbd>--brand</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "contains", "type": "contains",
"value": "var(--main-color)", "value": "steelblue",
"message": "Use <kbd>var(--main-color)</kbd>", "message": "Встановіть значення <kbd>steelblue</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "border", "expected": "var(--main-color)" },
"message": "Apply variable to border color",
"options": { "exact": false }
} }
] ]
}, },
{ {
"id": "units-3", "id": "units-3",
"title": "Unit Calculations (calc)", "title": "calc() Function",
"description": "Use the <kbd>calc()</kbd> function to combine different units in one expression.", "description": "Функція <kbd>calc()</kbd> дозволяє змішувати різні одиниці в обчисленнях. Це важливо для макетів, що поєднують фіксовані та гнучкі розміри, як макет із сайдбаром.",
"task": "Set the <kbd>width</kbd> of <kbd>.sized</kbd> to <kbd>calc(100% - 2rem)</kbd> and <kbd>min-height</kbd> to <kbd>calc(10vh + 1rem)</kbd>.", "task": "Основний контент повинен заповнити залишок місця після сайдбару 200px. Встановіть <kbd>width: calc(100% - 200px)</kbd> на <kbd>.main</kbd>.",
"previewHTML": "<div class=\"sized\">Calc Demo</div>", "previewHTML": "<div class=\"layout\"><aside class=\"sidebar\">Sidebar<br>Navigation</aside><main class=\"main\"><h2>Main Content</h2><p>This area should fill the remaining width after accounting for the fixed-width sidebar.</p></main></div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sized { background: #e8f5e9; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .layout { display: flex; gap: 1rem; } .sidebar { width: 200px; background: #1a1a2e; color: white; padding: 1rem; border-radius: 8px; flex-shrink: 0; } .main { background: white; padding: 1rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .main h2 { margin: 0 0 8px; } .main p { margin: 0; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Use calc for dynamic sizing */\n.sized {", "codePrefix": ".main {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": " width: calc(100% - 2rem);\n min-height: calc(10vh + 1rem);", "solution": "width: calc(100% - 200px);",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ "type": "contains", "value": "calc", "message": "Use <kbd>calc()</kbd> function", "options": { "caseSensitive": false } },
{ {
"type": "regex", "type": "regex",
"value": "width:\\s*calc\\(100% - 2rem\\)", "value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
"message": "Width should be <kbd>calc(100% - 2rem)</kbd>", "message": "Встановіть <kbd>width: calc(100% - 200px)</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "regex",
"value": "min-height:\\s*calc\\(10vh \\+ 1rem\\)",
"message": "Min-height should be <kbd>calc(10vh + 1rem)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
}, },
{ {
"id": "units-4", "id": "units-4",
"title": "Viewport & Responsive Units", "title": "Viewport Units",
"description": "Control layouts relative to viewport size with vw, vh, and vmin/vmax units.", "description": "Одиниці viewport розміряють елементи відносно вікна браузера:<br>• <kbd>vw</kbd> 1% ширини viewport<br>• <kbd>vh</kbd> 1% висоти viewport<br><br>Вони ідеальні для повноекранних секцій як hero-банери.",
"task": "Give <kbd>.view</kbd> a <kbd>width</kbd> of <kbd>50vw</kbd> and <kbd>height</kbd> of <kbd>20vh</kbd>.", "task": "Зробіть цю hero-секцію висотою з viewport, встановивши <kbd>min-height: 100vh</kbd>.",
"previewHTML": "<div class=\"view\">Viewport Box</div>", "previewHTML": "<section class=\"hero\"><h1>Welcome</h1><p>Scroll down to explore</p></section>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .view { background: #ffe0b2; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .hero { background: linear-gradient(135deg, #1a1a2e 0%, steelblue 100%); color: white; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 2rem; } .hero h1 { margin: 0 0 1rem; font-size: 2.5rem; } .hero p { margin: 0; opacity: 0.8; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Use viewport units */\n.view {", "codePrefix": ".hero {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": " width: 50vw;\n height: 20vh;", "solution": "min-height: 100vh;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ "type": "contains", "value": "vw", "message": "Use <kbd>vw</kbd> unit", "options": { "caseSensitive": false } }, {
{ "type": "contains", "value": "vh", "message": "Use <kbd>vh</kbd> unit", "options": { "caseSensitive": false } }, "type": "property_value",
{ "type": "property_value", "value": { "property": "width", "expected": "50vw" }, "message": "Set width to <kbd>50vw</kbd>" }, "value": { "property": "min-height", "expected": "100vh" },
{ "type": "property_value", "value": { "property": "height", "expected": "20vh" }, "message": "Set height to <kbd>20vh</kbd>" } "message": "Встановіть <kbd>min-height: 100vh</kbd>"
}
] ]
} }
] ]

View File

@@ -1,15 +1,15 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "transitions-animations", "id": "transitions-animations",
"title": "CSS Animations", "title": "CSS Анімації",
"description": "Bring interactivity to your UI by smoothly transitioning properties and creating keyframe-driven animations.", "description": "Додайте інтерактивність до інтерфейсу через плавні переходи властивостей та анімації на основі keyframes.",
"difficulty": "intermediate", "difficulty": "intermediate",
"lessons": [ "lessons": [
{ {
"id": "transitions-1", "id": "transitions-1",
"title": "Transitions", "title": "Transitions",
"description": "Learn how to apply <kbd>transition</kbd> to properties for smooth changes on state changes.<br><br><pre>transition: property duration;\n/* e.g. transition: background-color 0.3s; */</pre>", "description": "Навчіться застосовувати <kbd>transition</kbd> до властивостей для плавних змін при зміні стану.<br><br><pre>transition: property duration;\n/* напр. transition: background-color 0.3s; */</pre>",
"task": "Add <kbd>transition: background-color 0.3s</kbd> to <kbd>.btn</kbd> so the color fades smoothly on hover.", "task": "Додайте <kbd>transition: background-color 0.3s</kbd>, щоб колір плавно змінювався при наведенні.",
"previewHTML": "<button class=\"btn\">Hover Me</button>", "previewHTML": "<button class=\"btn\">Hover Me</button>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: black; color: white; padding: 0.5rem 1rem; border: none; cursor: pointer; } .btn:hover { background: white; color: black; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: black; color: white; padding: 0.5rem 1rem; border: none; cursor: pointer; } .btn:hover { background: white; color: black; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -22,13 +22,13 @@
{ {
"type": "contains", "type": "contains",
"value": "transition", "value": "transition",
"message": "Use the <kbd>transition</kbd> property", "message": "Використайте властивість <kbd>transition</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "regex", "type": "regex",
"value": "transition:\\s*background-color\\s*0\\.3s", "value": "transition:\\s*background-color\\s*0\\.3s",
"message": "Set <kbd>transition: background-color 0.3s</kbd>", "message": "Встановіть <kbd>transition: background-color 0.3s</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
@@ -36,8 +36,8 @@
{ {
"id": "transitions-2", "id": "transitions-2",
"title": "Timing Funcs", "title": "Timing Funcs",
"description": "Explore easing functions like <kbd>ease</kbd>, <kbd>linear</kbd>, <kbd>ease-in</kbd>, <kbd>ease-out</kbd> to control animation pacing.", "description": "Дослідіть функції пом'якшення як <kbd>ease</kbd>, <kbd>linear</kbd>, <kbd>ease-in</kbd>, <kbd>ease-out</kbd> для контролю темпу анімації.",
"task": "Set <kbd>transition-timing-function</kbd> to <kbd>ease-in-out</kbd> on <kbd>.btn</kbd>.", "task": "Встановіть <kbd>transition-timing-function</kbd> на <kbd>ease-in-out</kbd>.",
"previewHTML": "<button class=\"btn\">Timing</button>", "previewHTML": "<button class=\"btn\">Timing</button>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: navy; color: gold; padding: 0.5rem 1rem; border: none; cursor: pointer; transition: all 0.5s; } .btn:hover { background: gold; color: navy; transform: scale(1.1); }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .btn { background: navy; color: gold; padding: 0.5rem 1rem; border: none; cursor: pointer; transition: all 0.5s; } .btn:hover { background: gold; color: navy; transform: scale(1.1); }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -50,21 +50,21 @@
{ {
"type": "contains", "type": "contains",
"value": "transition-timing-function", "value": "transition-timing-function",
"message": "Use <kbd>transition-timing-function</kbd>", "message": "Використайте <kbd>transition-timing-function</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "transition-timing-function", "expected": "ease-in-out" }, "value": { "property": "transition-timing-function", "expected": "ease-in-out" },
"message": "Set timing to <kbd>ease-in-out</kbd>" "message": "Встановіть timing на <kbd>ease-in-out</kbd>"
} }
] ]
}, },
{ {
"id": "transitions-3", "id": "transitions-3",
"title": "Keyframes", "title": "Keyframes",
"description": "Create named animations using <kbd>@keyframes</kbd> and apply them via the <kbd>animation</kbd> shorthand.<br><br><pre>@keyframes bounce {\n 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;\n}</pre>", "description": "Створюйте іменовані анімації використовуючи <kbd>@keyframes</kbd> та застосовуйте їх через скорочення <kbd>animation</kbd>.<br><br><pre>@keyframes bounce {\n 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;\n}</pre>",
"task": "Define a keyframe at <kbd>50%</kbd> with <kbd>transform: translateY(-20px)</kbd> and apply <kbd>animation: bounce 1s infinite</kbd> to <kbd>.ball</kbd>.", "task": "Визначте keyframe при <kbd>50%</kbd> з <kbd>transform: translateY(-20px)</kbd> та застосуйте <kbd>animation: bounce 1s infinite</kbd> на <kbd>.ball</kbd>.",
"previewHTML": "<div class=\"ball\"></div>", "previewHTML": "<div class=\"ball\"></div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .ball { width: 50px; height: 50px; background: crimson; border-radius: 50%; margin: 2rem auto; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .ball { width: 50px; height: 50px; background: crimson; border-radius: 50%; margin: 2rem auto; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -77,25 +77,25 @@
{ {
"type": "contains", "type": "contains",
"value": "@keyframes bounce", "value": "@keyframes bounce",
"message": "Define <kbd>@keyframes bounce</kbd>", "message": "Визначте <kbd>@keyframes bounce</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "regex", "type": "regex",
"value": "50%.*transform: translateY\\(-20px\\)", "value": "50%.*transform: translateY\\(-20px\\)",
"message": "At <kbd>50%</kbd>, use <kbd>transform: translateY(-20px)</kbd>", "message": "При <kbd>50%</kbd>, використайте <kbd>transform: translateY(-20px)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "contains", "type": "contains",
"value": "animation", "value": "animation",
"message": "Use <kbd>animation</kbd> property on <kbd>.ball</kbd>", "message": "Використайте властивість <kbd>animation</kbd> на <kbd>.ball</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "regex", "type": "regex",
"value": "animation:.*bounce.*1s.*infinite", "value": "animation:.*bounce.*1s.*infinite",
"message": "Apply <kbd>animation: bounce 1s infinite</kbd>", "message": "Застосуйте <kbd>animation: bounce 1s infinite</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
} }
] ]
@@ -103,8 +103,8 @@
{ {
"id": "transitions-4", "id": "transitions-4",
"title": "Animation Properties", "title": "Animation Properties",
"description": "Fine-tune animations with <kbd>animation-delay</kbd>, <kbd>animation-iteration-count</kbd>, <kbd>animation-direction</kbd>, and <kbd>animation-fill-mode</kbd>.", "description": "Налаштуйте анімації за допомогою <kbd>animation-delay</kbd>, <kbd>animation-iteration-count</kbd>, <kbd>animation-direction</kbd> та <kbd>animation-fill-mode</kbd>.",
"task": "Apply the <kbd>pulse</kbd> animation to <kbd>.box</kbd> with <kbd>animation-name: pulse</kbd>, <kbd>animation-duration: 2s</kbd>, <kbd>animation-delay: 1s</kbd>, <kbd>animation-iteration-count: 2</kbd>, and <kbd>animation-fill-mode: forwards</kbd>.", "task": "Застосуйте анімацію <kbd>pulse</kbd> до <kbd>.box</kbd> з <kbd>animation-name: pulse</kbd>, <kbd>animation-duration: 2s</kbd>, <kbd>animation-delay: 1s</kbd>, <kbd>animation-iteration-count: 2</kbd> та <kbd>animation-fill-mode: forwards</kbd>.",
"previewHTML": "<div class=\"box\">Pulse</div>", "previewHTML": "<div class=\"box\">Pulse</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { width: 100px; height: 100px; background: black; color: white; display: flex; align-items: center; justify-content: center; margin: 2rem auto; } @keyframes pulse { 0% { background: black; color: white; transform: scale(1); } 50% { background: white; color: black; transform: scale(1.2); } 100% { background: limegreen; color: black; transform: scale(1); } }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { width: 100px; height: 100px; background: black; color: white; display: flex; align-items: center; justify-content: center; margin: 2rem auto; } @keyframes pulse { 0% { background: black; color: white; transform: scale(1); } 50% { background: white; color: black; transform: scale(1.2); } 100% { background: limegreen; color: black; transform: scale(1); } }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -117,27 +117,27 @@
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-name", "expected": "pulse" }, "value": { "property": "animation-name", "expected": "pulse" },
"message": "Set <kbd>animation-name: pulse</kbd>" "message": "Встановіть <kbd>animation-name: pulse</kbd>"
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-duration", "expected": "2s" }, "value": { "property": "animation-duration", "expected": "2s" },
"message": "Set <kbd>animation-duration: 2s</kbd>" "message": "Встановіть <kbd>animation-duration: 2s</kbd>"
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-delay", "expected": "1s" }, "value": { "property": "animation-delay", "expected": "1s" },
"message": "Set <kbd>animation-delay: 1s</kbd>" "message": "Встановіть <kbd>animation-delay: 1s</kbd>"
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-iteration-count", "expected": "2" }, "value": { "property": "animation-iteration-count", "expected": "2" },
"message": "Set <kbd>animation-iteration-count: 2</kbd>" "message": "Встановіть <kbd>animation-iteration-count: 2</kbd>"
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "animation-fill-mode", "expected": "forwards" }, "value": { "property": "animation-fill-mode", "expected": "forwards" },
"message": "Set <kbd>animation-fill-mode: forwards</kbd>" "message": "Встановіть <kbd>animation-fill-mode: forwards</kbd>"
} }
] ]
} }

View File

@@ -2,14 +2,14 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "responsive-design", "id": "responsive-design",
"title": "CSS Responsive Design", "title": "CSS Responsive Design",
"description": "Make your layouts adapt to different screen sizes using media queries and fluid design techniques.", "description": "Адаптуйте ваші макети до різних розмірів екранів використовуючи media queries та техніки плавного дизайну.",
"difficulty": "intermediate", "difficulty": "intermediate",
"lessons": [ "lessons": [
{ {
"id": "responsive-1", "id": "responsive-1",
"title": "Media Queries", "title": "Media Queries",
"description": "Understand the syntax and use cases for CSS media queries to apply styles conditionally based on viewport characteristics.", "description": "Зрозумійте синтаксис та випадки використання CSS media queries для умовного застосування стилів на основі характеристик viewport.<br><br><pre>@media (max-width: 600px) {\n .panel {\n background: lightcoral;\n }\n}</pre>",
"task": "Write a media query with <kbd>@media (max-width: 600px)</kbd> that changes <kbd>.panel</kbd> background to <kbd>lightcoral</kbd>.", "task": "Напишіть media query з <kbd>@media (max-width: 600px)</kbd>, яка змінює фон <kbd>.panel</kbd> на <kbd>lightcoral</kbd>.",
"previewHTML": "<div class=\"panel\">Resize the window</div>", "previewHTML": "<div class=\"panel\">Resize the window</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .panel { padding: 1rem; background: lightblue; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .panel { padding: 1rem; background: lightblue; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -22,19 +22,19 @@
{ {
"type": "regex", "type": "regex",
"value": "@media\\s*\\(max-width:\\s*600px\\)", "value": "@media\\s*\\(max-width:\\s*600px\\)",
"message": "Use <kbd>@media (max-width: 600px)</kbd>", "message": "Використайте <kbd>@media (max-width: 600px)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "contains", "type": "contains",
"value": ".panel", "value": ".panel",
"message": "Target <kbd>.panel</kbd> inside the media query", "message": "Вкажіть <kbd>.panel</kbd> всередині media query",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "background", "expected": "lightcoral" }, "value": { "property": "background", "expected": "lightcoral" },
"message": "Set <kbd>background: lightcoral</kbd>", "message": "Встановіть <kbd>background: lightcoral</kbd>",
"options": { "exact": false } "options": { "exact": false }
} }
] ]
@@ -42,8 +42,8 @@
{ {
"id": "responsive-2", "id": "responsive-2",
"title": "Fluid Type", "title": "Fluid Type",
"description": "Use relative units like <kbd>vw</kbd> to make font sizes scale with the viewport width.", "description": "Використовуйте відносні одиниці як <kbd>vw</kbd>, щоб розміри шрифтів масштабувались з шириною viewport.",
"task": "Set <kbd>font-size: 5vw</kbd> on <kbd>.text</kbd> so it scales as the viewport changes.", "task": "Встановіть <kbd>font-size: 5vw</kbd>, щоб масштабувалась з viewport.",
"previewHTML": "<p class=\"text\">Fluid Typography</p>", "previewHTML": "<p class=\"text\">Fluid Typography</p>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -53,46 +53,50 @@
"solution": " font-size: 5vw;", "solution": " font-size: 5vw;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "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": "Встановіть <kbd>font-size: 5vw</kbd>"
}
] ]
}, },
{ {
"id": "responsive-3", "id": "responsive-3",
"title": "Flex Grids", "title": "Responsive Grid",
"description": "Combine CSS Grid with <kbd>auto-fit</kbd> or <kbd>auto-fill</kbd> for responsive column layouts.", "description": "Поєднайте CSS Grid з <kbd>auto-fit</kbd> або <kbd>auto-fill</kbd> для адаптивних колонкових макетів, які автоматично налаштовують кількість колонок на основі доступного простору.",
"task": "Add <kbd>display: grid</kbd>, <kbd>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</kbd>, and <kbd>gap: 1rem</kbd> to <kbd>.cards</kbd>.", "task": "Додайте <kbd>display: grid</kbd>, <kbd>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</kbd> та <kbd>gap: 1rem</kbd>.",
"previewHTML": "<div class=\"cards\"><div>1</div><div>2</div><div>3</div><div>4</div></div>", "previewHTML": "<section class=\"features\"><article class=\"feature\"><h3>Fast</h3><p>Lightning quick load times</p></article><article class=\"feature\"><h3>Secure</h3><p>Enterprise-grade security</p></article><article class=\"feature\"><h3>Reliable</h3><p>99.9% uptime guaranteed</p></article><article class=\"feature\"><h3>Support</h3><p>24/7 customer service</p></article></section>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .cards > div { background: #d1c4e9; padding: 1rem; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .feature { background: white; padding: 1rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .feature h3 { margin: 0 0 8px; color: steelblue; } .feature p { margin: 0; color: #666; font-size: 0.9rem; }",
"sandboxCSS": "", "sandboxCSS": "",
"codePrefix": "/* Create a responsive grid */\n.cards {", "codePrefix": ".features {\n ",
"initialCode": "", "initialCode": "",
"codeSuffix": "}", "codeSuffix": "\n}",
"solution": "display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;", "solution": "display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "display", "expected": "grid" }, "value": { "property": "display", "expected": "grid" },
"message": "Set <kbd>display: grid</kbd>" "message": "Встановіть <kbd>display: grid</kbd>"
}, },
{ {
"type": "regex", "type": "regex",
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)", "value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
"message": "Use <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>", "message": "Використайте <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "gap", "expected": "1rem" }, "value": { "property": "gap", "expected": "1rem" },
"message": "Set <kbd>gap: 1rem</kbd>" "message": "Встановіть <kbd>gap: 1rem</kbd>"
} }
] ]
}, },
{ {
"id": "responsive-4", "id": "responsive-4",
"title": "Mobile-First", "title": "Mobile-First",
"description": "Adopt a mobile-first approach by writing base styles for small screens and enhancing for larger viewports.", "description": "Застосуйте підхід mobile-first: пишіть базові стилі для малих екранів і розширюйте для більших viewport.",
"task": "Write a media query with <kbd>@media (min-width: 768px)</kbd> that sets <kbd>.sidebar</kbd> width to <kbd>250px</kbd>.", "task": "Напишіть media query з <kbd>@media (min-width: 768px)</kbd>, яка встановлює ширину <kbd>.sidebar</kbd> на <kbd>250px</kbd>.",
"previewHTML": "<aside class=\"sidebar\">Sidebar</aside>", "previewHTML": "<aside class=\"sidebar\">Sidebar</aside>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -105,19 +109,19 @@
{ {
"type": "regex", "type": "regex",
"value": "@media\\s*\\(min-width:\\s*768px\\)", "value": "@media\\s*\\(min-width:\\s*768px\\)",
"message": "Use <kbd>@media (min-width: 768px)</kbd>", "message": "Використайте <kbd>@media (min-width: 768px)</kbd>",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "contains", "type": "contains",
"value": ".sidebar", "value": ".sidebar",
"message": "Target <kbd>.sidebar</kbd> inside media query", "message": "Вкажіть <kbd>.sidebar</kbd> в media query",
"options": { "caseSensitive": false } "options": { "caseSensitive": false }
}, },
{ {
"type": "property_value", "type": "property_value",
"value": { "property": "width", "expected": "250px" }, "value": { "property": "width", "expected": "250px" },
"message": "Set <kbd>width: 250px</kbd>", "message": "Встановіть <kbd>width: 250px</kbd>",
"options": { "exact": false } "options": { "exact": false }
} }
] ]

View File

@@ -2,65 +2,65 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-elements", "id": "html-elements",
"title": "HTML Block & Inline", "title": "HTML Block & Inline",
"description": "Understanding the fundamental difference between container (block) and inline elements", "description": "Зрозумійте основну різницю між контейнерними (блоковими) та рядковими елементами",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "block-vs-inline-intro", "id": "block-vs-inline-intro",
"title": "Block vs Inline Elements", "title": "Блокові vs рядкові елементи",
"description": "HTML elements fall into two main categories:<br><br><strong>Block elements</strong> (containers) start on a new line and take full width. Examples: <kbd>&lt;div&gt;</kbd>, <kbd>&lt;p&gt;</kbd>, <kbd>&lt;h1&gt;</kbd>, <kbd>&lt;section&gt;</kbd><br><br><strong>Inline elements</strong> flow within text and only take needed width. Examples: <kbd>&lt;span&gt;</kbd>, <kbd>&lt;a&gt;</kbd>, <kbd>&lt;strong&gt;</kbd>, <kbd>&lt;em&gt;</kbd>", "description": "HTML елементи поділяються на дві основні категорії:<br><br><strong>Блокові елементи</strong> (контейнери) починаються з нового рядка і займають повну ширину. Приклади: <kbd>&lt;div&gt;</kbd>, <kbd>&lt;p&gt;</kbd>, <kbd>&lt;h1&gt;</kbd>, <kbd>&lt;section&gt;</kbd><br><br><strong>Рядкові елементи</strong> розміщуються всередині тексту і займають лише необхідну ширину. Приклади: <kbd>&lt;span&gt;</kbd>, <kbd>&lt;a&gt;</kbd>, <kbd>&lt;strong&gt;</kbd>, <kbd>&lt;em&gt;</kbd>",
"task": "Wrap the word <kbd>important</kbd> with <kbd>&lt;strong&gt;</kbd> tags to make it bold. Notice how the paragraph (block) takes full width while strong (inline) flows with text.", "task": "Оберніть слово <kbd>важливе</kbd> тегами <kbd>&lt;strong&gt;</kbd>, щоб зробити його жирним. Зверніть увагу, як абзац (блок) займає повну ширину, тоді як strong (рядковий) тече з текстом.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; } p { background: #e3f2fd; padding: 10px; } strong { background: #ffecb3; }", "previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; } p { background: #e3f2fd; padding: 10px; } strong { background: #ffecb3; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<p>This is a paragraph with an important word.</p>", "initialCode": "<p>Це абзац з важливе словом.</p>",
"solution": "<p>This is a paragraph with an <strong>important</strong> word.</p>", "solution": "<p>Це абзац з <strong>важливе</strong> словом.</p>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "p", "value": "p",
"message": "Add a <kbd>&lt;p&gt;</kbd> paragraph element" "message": "Додайте елемент абзацу <kbd>&lt;p&gt;</kbd>"
}, },
{ {
"type": "parent_child", "type": "parent_child",
"value": { "parent": "p", "child": "strong" }, "value": { "parent": "p", "child": "strong" },
"message": "Wrap the word <kbd>important</kbd> with <kbd>&lt;strong&gt;</kbd> tags" "message": "Оберніть слово <kbd>важливе</kbd> тегами <kbd>&lt;strong&gt;</kbd>"
} }
] ]
}, },
{ {
"id": "semantic-containers", "id": "semantic-containers",
"title": "Semantic Tags", "title": "Семантичні теги",
"description": "Modern HTML uses semantic containers that describe their content:<br><br><kbd>&lt;header&gt;</kbd> - Page or section header<br><kbd>&lt;nav&gt;</kbd> - Navigation links<br><kbd>&lt;main&gt;</kbd> - Main content area<br><kbd>&lt;section&gt;</kbd> - Thematic grouping<br><kbd>&lt;article&gt;</kbd> - Self-contained content<br><kbd>&lt;footer&gt;</kbd> - Page or section footer", "description": "Сучасний HTML використовує семантичні контейнери, які описують свій вміст:<br><br><kbd>&lt;header&gt;</kbd> - Заголовок сторінки або секції<br><kbd>&lt;nav&gt;</kbd> - Навігаційні посилання<br><kbd>&lt;main&gt;</kbd> - Основний вміст<br><kbd>&lt;section&gt;</kbd> - Тематичне групування<br><kbd>&lt;article&gt;</kbd> - Самостійний вміст<br><kbd>&lt;footer&gt;</kbd> - Підвал сторінки або секції",
"task": "Create a basic page structure:<br>1. Add a <kbd>&lt;header&gt;</kbd> with an <kbd>&lt;h1&gt;</kbd> containing the text <code>My Website</code><br>2. Add a <kbd>&lt;main&gt;</kbd> element with a paragraph saying <code>Welcome to my site!</code><br>3. Add a <kbd>&lt;footer&gt;</kbd> with a paragraph saying <code>Copyright 2026</code>", "task": "Створіть базову структуру сторінки:<br>1. Додайте <kbd>&lt;header&gt;</kbd> з <kbd>&lt;h1&gt;</kbd>, що містить текст <code>Мій Сайт</code><br>2. Додайте елемент <kbd>&lt;main&gt;</kbd> з абзацом, що каже <code>Ласкаво просимо на мій сайт!</code><br>3. Додайте <kbd>&lt;footer&gt;</kbd> з абзацом, що каже <code>Copyright 2026</code>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; margin: 0; } header { background: #1976d2; color: white; padding: 15px; } main { padding: 20px; min-height: 100px; } footer { background: #424242; color: white; padding: 10px; text-align: center; }", "previewBaseCSS": "body { font-family: system-ui; margin: 0; } header { background: #1976d2; color: white; padding: 15px; } main { padding: 20px; min-height: 100px; } footer { background: #424242; color: white; padding: 10px; text-align: center; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<header>\n <h1>My Website</h1>\n</header>\n<main>\n <p>Welcome to my site!</p>\n</main>\n<footer>\n <p>Copyright 2026</p>\n</footer>", "solution": "<header>\n <h1>Мій Сайт</h1>\n</header>\n<main>\n <p>Ласкаво просимо на мій сайт!</p>\n</main>\n<footer>\n <p>Copyright 2026</p>\n</footer>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "header", "value": "header",
"message": "Add a <kbd>&lt;header&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;header&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "main", "value": "main",
"message": "Add a <kbd>&lt;main&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;main&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "footer", "value": "footer",
"message": "Add a <kbd>&lt;footer&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;footer&gt;</kbd>"
}, },
{ {
"type": "parent_child", "type": "parent_child",
"value": { "parent": "header", "child": "h1" }, "value": { "parent": "header", "child": "h1" },
"message": "Add an <kbd>&lt;h1&gt;</kbd> heading inside your header" "message": "Додайте заголовок <kbd>&lt;h1&gt;</kbd> всередині header"
} }
] ]
} }

View File

@@ -1,100 +1,100 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-basic", "id": "html-forms-basic",
"title": "HTML Forms", "title": "HTML Форми",
"description": "Learn to create forms with various input types", "description": "Навчіться створювати форми з різними типами полів",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "form-structure", "id": "form-structure",
"title": "Form Structure", "title": "Структура форми",
"description": "Every form needs a <kbd>&lt;form&gt;</kbd> wrapper. Inside, use <kbd>&lt;label&gt;</kbd> to describe inputs and <kbd>&lt;input&gt;</kbd> for user data entry.<br><br>The <kbd>for</kbd> attribute on labels should match the <kbd>id</kbd> on inputs for accessibility.", "description": "Кожна форма потребує обгортки <kbd>&lt;form&gt;</kbd>. Всередині використовуйте <kbd>&lt;label&gt;</kbd> для опису полів та <kbd>&lt;input&gt;</kbd> для введення даних.<br><br>Атрибут <kbd>for</kbd> у label має відповідати <kbd>id</kbd> полів для доступності.",
"task": "Create a form with:<br>1. A <kbd>&lt;label&gt;</kbd> with the text <code>Name:</code> and <kbd>for=\"name\"</kbd> attribute<br>2. A text <kbd>&lt;input&gt;</kbd> with <kbd>id=\"name\"</kbd> and <kbd>name=\"name\"</kbd> attributes", "task": "Створіть форму з:<br>1. <kbd>&lt;label&gt;</kbd> з текстом <code>Ім'я:</code> та атрибутом <kbd>for=\"name\"</kbd><br>2. Текстовим <kbd>&lt;input&gt;</kbd> з атрибутами <kbd>id=\"name\"</kbd> та <kbd>name=\"name\"</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-bottom: 5px; font-weight: 500; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-bottom: 5px; font-weight: 500; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<form>\n <label for=\"name\">Name:</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n</form>", "solution": "<form>\n <label for=\"name\">Ім'я:</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n</form>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "form", "value": "form",
"message": "Wrap everything in a <kbd>&lt;form&gt;</kbd> element" "message": "Оберніть все елементом <kbd>&lt;form&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "label", "value": "label",
"message": "Add a <kbd>&lt;label&gt;</kbd> for your input" "message": "Додайте <kbd>&lt;label&gt;</kbd> для вашого поля"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "input", "value": "input",
"message": "Add an <kbd>&lt;input&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;input&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "label", "attr": "for", "value": null }, "value": { "selector": "label", "attr": "for", "value": null },
"message": "Add a <kbd>for</kbd> attribute to your label" "message": "Додайте атрибут <kbd>for</kbd> до вашого label"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input", "attr": "id", "value": null }, "value": { "selector": "input", "attr": "id", "value": null },
"message": "Add an <kbd>id</kbd> attribute to your input" "message": "Додайте атрибут <kbd>id</kbd> до вашого поля"
} }
] ]
}, },
{ {
"id": "input-types", "id": "input-types",
"title": "Input Types", "title": "Типи полів",
"description": "Different input types provide appropriate keyboards and validation:<br><br><kbd>type=\"text\"</kbd> - General text<br><kbd>type=\"email\"</kbd> - Email with @ validation<br><kbd>type=\"password\"</kbd> - Hidden characters<br><kbd>type=\"number\"</kbd> - Numeric keyboard<br><kbd>type=\"tel\"</kbd> - Phone keyboard", "description": "Різні типи полів надають відповідні клавіатури та валідацію:<br><br><kbd>type=\"text\"</kbd> - Загальний текст<br><kbd>type=\"email\"</kbd> - Email з валідацією @<br><kbd>type=\"password\"</kbd> - Приховані символи<br><kbd>type=\"number\"</kbd> - Цифрова клавіатура<br><kbd>type=\"tel\"</kbd> - Телефонна клавіатура",
"task": "Create a login form with two fields:<br>1. An email field: <kbd>&lt;label for=\"email\"&gt;Email:&lt;/label&gt;</kbd> and <kbd>&lt;input type=\"email\" id=\"email\"&gt;</kbd><br>2. A password field: <kbd>&lt;label for=\"password\"&gt;Password:&lt;/label&gt;</kbd> and <kbd>&lt;input type=\"password\" id=\"password\"&gt;</kbd>", "task": "Створіть форму входу з двома полями:<br>1. Поле email: <kbd>&lt;label for=\"email\"&gt;Email:&lt;/label&gt;</kbd> та <kbd>&lt;input type=\"email\" id=\"email\"&gt;</kbd><br>2. Поле пароля: <kbd>&lt;label for=\"password\"&gt;Пароль:&lt;/label&gt;</kbd> та <kbd>&lt;input type=\"password\" id=\"password\"&gt;</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<form>\n \n</form>", "initialCode": "<form>\n \n</form>",
"solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\">\n</form>", "solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <label for=\"password\">Пароль:</label>\n <input type=\"password\" id=\"password\" name=\"password\">\n</form>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "input[type='email']", "value": "input[type='email']",
"message": "Add an input with type=\"email\"" "message": "Додайте поле з type=\"email\""
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "input[type='password']", "value": "input[type='password']",
"message": "Add an input with type=\"password\"" "message": "Додайте поле з type=\"password\""
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "label", "min": 2 }, "value": { "selector": "label", "min": 2 },
"message": "Add labels for both inputs" "message": "Додайте label для обох полів"
} }
] ]
}, },
{ {
"id": "submit-button", "id": "submit-button",
"title": "Submit Button", "title": "Кнопка відправки",
"description": "Forms need a way to submit data. Use:<br><br><kbd>&lt;button type=\"submit\"&gt;</kbd> - Preferred, flexible content<br><kbd>&lt;input type=\"submit\"&gt;</kbd> - Simple text-only button<br><br>The button text should be action-oriented (e.g., <code>Sign In</code>, 'Register', 'Send').", "description": "Форми потребують способу відправки даних. Використовуйте:<br><br><kbd>&lt;button type=\"submit\"&gt;</kbd> - Переважно, гнучкий вміст<br><kbd>&lt;input type=\"submit\"&gt;</kbd> - Простий текстовий кнопка<br><br>Текст кнопки має бути орієнтованим на дію (напр. <code>Увійти</code>, 'Зареєструватись', 'Надіслати').",
"task": "Add a submit button to the form with the text <code>Sign In</code>.", "task": "Додайте кнопку відправки до форми з текстом <code>Увійти</code>.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } button { width: 100%; margin-top: 20px; padding: 10px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } button:hover { background: #1565c0; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 300px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-child { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } button { width: 100%; margin-top: 20px; padding: 10px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } button:hover { background: #1565c0; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n \n</form>", "initialCode": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Пароль:</label>\n <input type=\"password\" id=\"password\">\n \n</form>",
"solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\">\n \n <button type=\"submit\">Sign In</button>\n</form>", "solution": "<form>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">Пароль:</label>\n <input type=\"password\" id=\"password\">\n \n <button type=\"submit\">Увійти</button>\n</form>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "button[type='submit'], input[type='submit']", "value": "button[type='submit'], input[type='submit']",
"message": "Add a submit button to your form" "message": "Додайте кнопку відправки до форми"
}, },
{ {
"type": "element_text", "type": "element_text",
"value": { "selector": "button", "text": "Sign In" }, "value": { "selector": "button", "text": "Увійти" },
"message": "The button should say <kbd>Sign In</kbd>" "message": "Кнопка має відображати <kbd>Увійти</kbd>"
} }
] ]
} }

View File

@@ -1,110 +1,32 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-validation", "id": "html-forms-validation",
"title": "HTML Validation", "title": "Валідація форм",
"description": "Learn HTML5 built-in form validation attributes", "description": "Використовуйте вбудовану валідацію HTML5 для кращого досвіду користувача",
"mode": "html", "mode": "html",
"difficulty": "intermediate", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "required-fields", "id": "required-fields",
"title": "Required Fields", "title": "Обов'язкові поля",
"description": "The <kbd>required</kbd> attribute prevents form submission if the field is empty.<br><br>Add it to any input that must be filled:<br><kbd>&lt;input type=\"text\" required&gt;</kbd><br><br>The browser shows a validation message automatically.", "description": "Атрибут <kbd>required</kbd> запобігає відправці форми, якщо поле порожнє. Браузер автоматично показує повідомлення валідації - без JavaScript!<br><br>Додайте його до будь-якого поля, яке має бути заповнене:<br><kbd>&lt;input type=\"text\" required&gt;</kbd>",
"task": "Make both the name and email fields required by adding the <kbd>required</kbd> attribute.", "task": "Зробіть обидва поля (ім'я та email) обов'язковими, додавши атрибут <kbd>required</kbd> до кожного поля.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } label { display: block; margin-top: 15px; margin-bottom: 5px; } label:first-of-type { margin-top: 0; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid { border-color: #d32f2f; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 320px; } label { display: block; margin-top: 15px; margin-bottom: 5px; font-weight: 500; } label:first-of-type { margin-top: 0; } input { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; } input:focus { outline: 2px solid steelblue; border-color: transparent; } button { margin-top: 20px; padding: 10px 20px; background: steelblue; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 500; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n \n <label for=\"email\">Email: *</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <button type=\"submit\">Submit</button>\n</form>", "initialCode": "<form>\n <label for=\"name\">Ім'я *</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n \n <label for=\"email\">Email *</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <button type=\"submit\">Надіслати</button>\n</form>",
"solution": "<form>\n <label for=\"name\">Name: *</label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n \n <label for=\"email\">Email: *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <button type=\"submit\">Submit</button>\n</form>", "solution": "<form>\n <label for=\"name\">Ім'я *</label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n \n <label for=\"email\">Email *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <button type=\"submit\">Надіслати</button>\n</form>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input[name='name']", "attr": "required", "value": true }, "value": { "selector": "input[name='name']", "attr": "required", "value": true },
"message": "Add the <kbd>required</kbd> attribute to the name input" "message": "Додайте <kbd>required</kbd> до поля імені"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input[name='email']", "attr": "required", "value": true }, "value": { "selector": "input[name='email']", "attr": "required", "value": true },
"message": "Add the <kbd>required</kbd> attribute to the email input" "message": "Додайте <kbd>required</kbd> до поля email"
}
]
},
{
"id": "input-constraints",
"title": "Constraints",
"description": "Control what users can enter:<br><br><kbd>minlength</kbd> / <kbd>maxlength</kbd> - Text length limits<br><kbd>min</kbd> / <kbd>max</kbd> - Number range<br><kbd>pattern</kbd> - Regex pattern matching<br><kbd>placeholder</kbd> - Hint text (not a label!)",
"task": "Add validation to the password input:<br>1. Add <kbd>minlength=\"8\"</kbd> for minimum length<br>2. Add <kbd>maxlength=\"20\"</kbd> for maximum length<br>3. Add <kbd>placeholder=\"Enter password\"</kbd> as a hint",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 350px; } label { display: block; margin-bottom: 5px; } input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } input:invalid:not(:placeholder-shown) { border-color: #d32f2f; } small { display: block; font-size: 12px; color: #666; margin-top: 4px; } button { margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; }",
"sandboxCSS": "",
"initialCode": "<form>\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Must be 8-20 characters</small>\n \n <button type=\"submit\">Create Account</button>\n</form>",
"solution": "<form>\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\" maxlength=\"20\" placeholder=\"Enter password\" aria-describedby=\"password-hint\">\n <small id=\"password-hint\">Must be 8-20 characters</small>\n \n <button type=\"submit\">Create Account</button>\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "minlength", "value": "8" },
"message": "Add <kbd>minlength</kbd>=\"8\" to the password input"
},
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "maxlength", "value": "20" },
"message": "Add <kbd>maxlength</kbd>=\"20\" to the password input"
},
{
"type": "attribute_value",
"value": { "selector": "input[type='password']", "attr": "placeholder", "value": null },
"message": "Add a <kbd>placeholder</kbd> to hint what to enter"
}
]
},
{
"id": "complete-registration",
"title": "Full Form",
"description": "Build a complete registration form with all validation concepts:<br><br>- Required fields marked with *<br>- Email validation (use type=\"email\")<br>- Password with length constraints<br>- Terms checkbox (required)<br>- Submit button",
"task": "Complete the registration form. Add required attributes, proper input types, and validation constraints.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } form { max-width: 400px; background: #f5f5f5; padding: 25px; border-radius: 8px; } h2 { margin-top: 0; margin-bottom: 20px; } label { display: block; margin-top: 15px; margin-bottom: 5px; font-weight: 500; } input[type='text'], input[type='email'], input[type='password'] { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 14px; } input:focus { outline: 2px solid #1976d2; border-color: transparent; } input[type='checkbox'] { width: auto; margin-right: 8px; vertical-align: middle; } label:has(input[type='checkbox']) { display: flex; align-items: center; font-weight: normal; } button { width: 100%; margin-top: 20px; padding: 12px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 500; } button:hover { background: #1565c0; }",
"sandboxCSS": "",
"initialCode": "<form>\n <h2>Create Account</h2>\n \n <label for=\"fullname\">Full Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\">\n \n <label for=\"email\">Email *</label>\n <input id=\"email\" name=\"email\">\n \n <label for=\"password\">Password *</label>\n <input id=\"password\" name=\"password\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\">\n I agree to the Terms of Service *\n </label>\n \n <button type=\"submit\">Register</button>\n</form>",
"solution": "<form>\n <h2>Create Account</h2>\n \n <label for=\"fullname\">Full Name *</label>\n <input type=\"text\" id=\"fullname\" name=\"fullname\" required>\n \n <label for=\"email\">Email *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <label for=\"password\">Password *</label>\n <input type=\"password\" id=\"password\" name=\"password\" required minlength=\"8\">\n \n <label>\n <input type=\"checkbox\" id=\"terms\" name=\"terms\" required>\n I agree to the Terms of Service *\n </label>\n \n <button type=\"submit\">Register</button>\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "attribute_value",
"value": { "selector": "#fullname", "attr": "required", "value": true },
"message": "Make the full name field <kbd>required</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#email", "attr": "type", "value": "email" },
"message": "Set the email input <kbd>type=\"email\"</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#email", "attr": "required", "value": true },
"message": "Make the email field <kbd>required</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "type", "value": "password" },
"message": "Set the password input <kbd>type=\"password\"</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "required", "value": true },
"message": "Make the password field <kbd>required</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "minlength", "value": "8" },
"message": "Add <kbd>minlength</kbd>=\"8\" to password"
},
{
"type": "attribute_value",
"value": { "selector": "#terms", "attr": "required", "value": true },
"message": "Make the terms checkbox <kbd>required</kbd>"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-details-summary", "id": "html-details-summary",
"title": "HTML Details & Summary", "title": "HTML Details & Summary",
"description": "Create expandable content sections without JavaScript", "description": "Створюйте розгортувані секції без JavaScript",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "details-summary-basic", "id": "details-summary-basic",
"title": "First Widget", "title": "Перший віджет",
"description": "The <kbd>&lt;details&gt;</kbd> element creates a collapsible section. The <kbd>&lt;summary&gt;</kbd> provides the clickable label.<br><br>Click the summary to toggle the hidden content - no JavaScript needed!", "description": "Елемент <kbd>&lt;details&gt;</kbd> створює згортувану секцію. Елемент <kbd>&lt;summary&gt;</kbd> надає клікабельний заголовок.<br><br>Натисніть на summary, щоб показати прихований вміст - без JavaScript!",
"task": "Create a <kbd>&lt;details&gt;</kbd> element with:<br>1. A <kbd>&lt;summary&gt;</kbd> saying <code>Click to reveal</code><br>2. A <kbd>&lt;p&gt;</kbd> with the text <code>This content was hidden!</code>", "task": "Створіть елемент <kbd>&lt;details&gt;</kbd> з:<br>1. Елементом <kbd>&lt;summary&gt;</kbd> з текстом <code>Click to reveal</code><br>2. Елементом <kbd>&lt;p&gt;</kbd> з текстом <code>This content was hidden!</code>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; } summary { font-weight: 600; cursor: pointer; } summary:hover { color: #1976d2; } details p { margin-top: 15px; color: #666; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; } summary { font-weight: 600; cursor: pointer; } summary:hover { color: #1976d2; } details p { margin-top: 15px; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,30 +21,30 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "details", "value": "details",
"message": "Add a <kbd>&lt;details&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;details&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "summary", "value": "summary",
"message": "Add a <kbd>&lt;summary&gt;</kbd> inside the details" "message": "Додайте <kbd>&lt;summary&gt;</kbd> всередину details"
}, },
{ {
"type": "parent_child", "type": "parent_child",
"value": { "parent": "details", "child": "summary" }, "value": { "parent": "details", "child": "summary" },
"message": "The <kbd>&lt;summary&gt;</kbd> must be inside <kbd>&lt;details&gt;</kbd>" "message": "Елемент <kbd>&lt;summary&gt;</kbd> має бути всередині <kbd>&lt;details&gt;</kbd>"
}, },
{ {
"type": "parent_child", "type": "parent_child",
"value": { "parent": "details", "child": "p" }, "value": { "parent": "details", "child": "p" },
"message": "Add a <kbd>&lt;p&gt;</kbd> inside <kbd>&lt;details&gt;</kbd> for the hidden content" "message": "Додайте <kbd>&lt;p&gt;</kbd> всередину <kbd>&lt;details&gt;</kbd> для прихованого вмісту"
} }
] ]
}, },
{ {
"id": "details-open-attribute", "id": "details-open-attribute",
"title": "Pre-expanded Details", "title": "Розгорнуто за замовчуванням",
"description": "By default, <kbd>&lt;details&gt;</kbd> is closed. Add the <kbd>open</kbd> attribute to show the content initially.<br><br>This is a boolean attribute - just add <kbd>open</kbd> without a value.", "description": "За замовчуванням <kbd>&lt;details&gt;</kbd> закритий. Додайте атрибут <kbd>open</kbd>, щоб показати вміст спочатку.<br><br>Це булевий атрибут - просто додайте <kbd>open</kbd> без значення.",
"task": "Add the <kbd>open</kbd> attribute to the <kbd>&lt;details&gt;</kbd> element to show the content by default.", "task": "Додайте атрибут <kbd>open</kbd> до елемента <kbd>&lt;details&gt;</kbd>, щоб показати вміст за замовчуванням.",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; background: #f9f9f9; } summary { font-weight: 600; cursor: pointer; } details p { margin-top: 15px; color: #666; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } details { border: 1px solid #ddd; border-radius: 8px; padding: 15px; background: #f9f9f9; } summary { font-weight: 600; cursor: pointer; } details p { margin-top: 15px; color: #666; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -55,15 +55,15 @@
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "details", "attr": "open", "value": true }, "value": { "selector": "details", "attr": "open", "value": true },
"message": "Add the <kbd>open</kbd> attribute to <kbd>&lt;details&gt;</kbd>" "message": "Додайте атрибут <kbd>open</kbd> до <kbd>&lt;details&gt;</kbd>"
} }
] ]
}, },
{ {
"id": "faq-accordion", "id": "faq-accordion",
"title": "FAQ Accordion", "title": "FAQ акордеон",
"description": "Multiple <kbd>&lt;details&gt;</kbd> elements create an accordion-style FAQ. Each question can be expanded independently.<br><br><b>Pro tip:</b> Type <kbd>details*3&gt;summary+p</kbd> and press Tab for Emmet expansion. <kbd>*3</kbd> creates 3 elements, <kbd>&gt;</kbd> nests inside, <kbd>+</kbd> adds siblings.", "description": "Кілька елементів <kbd>&lt;details&gt;</kbd> створюють FAQ у стилі акордеону. Кожне питання можна розгортати незалежно.<br><br><b>Порада:</b> Введіть <kbd>details*3&gt;summary+p</kbd> і натисніть Tab для розгортання Emmet. <kbd>*3</kbd> створює 3 елементи, <kbd>&gt;</kbd> вкладає всередину, <kbd>+</kbd> додає сусідні.",
"task": "Create an FAQ section with:<br>1. An <kbd>&lt;h1&gt;</kbd> saying <code>Frequently Asked Questions</code><br>2. Three <kbd>&lt;details&gt;</kbd> elements, each with a question in <kbd>&lt;summary&gt;</kbd> and an answer in <kbd>&lt;p&gt;</kbd>", "task": "Створіть секцію FAQ з:<br>1. Заголовком <kbd>&lt;h1&gt;</kbd> з текстом <code>Frequently Asked Questions</code><br>2. Трьома елементами <kbd>&lt;details&gt;</kbd>, кожен з питанням у <kbd>&lt;summary&gt;</kbd> та відповіддю у <kbd>&lt;p&gt;</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; min-height: 100vh; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); display: flex; flex-direction: column; justify-content: center; padding: 40px; margin: 0; box-sizing: border-box; } h1 { font-size: 2.5rem; color: #4a4a4a; text-align: center; margin: 0 0 30px 0; } details { background: white; border-radius: 12px; margin-bottom: 15px; padding: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } summary { font-size: 1.3rem; font-weight: 600; color: #5c5c5c; cursor: pointer; list-style: none; } summary::before { content: '▸ '; color: #fcb69f; } details[open] summary::before { content: '▾ '; } details p { margin: 15px 0 0 0; color: #666; line-height: 1.6; }", "previewBaseCSS": "body { font-family: system-ui; min-height: 100vh; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); display: flex; flex-direction: column; justify-content: center; padding: 40px; margin: 0; box-sizing: border-box; } h1 { font-size: 2.5rem; color: #4a4a4a; text-align: center; margin: 0 0 30px 0; } details { background: white; border-radius: 12px; margin-bottom: 15px; padding: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } summary { font-size: 1.3rem; font-weight: 600; color: #5c5c5c; cursor: pointer; list-style: none; } summary::before { content: '▸ '; color: #fcb69f; } details[open] summary::before { content: '▾ '; } details p { margin: 15px 0 0 0; color: #666; line-height: 1.6; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -74,22 +74,22 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "h1", "value": "h1",
"message": "Add an <kbd>&lt;h1&gt;</kbd> heading for the FAQ title" "message": "Додайте заголовок <kbd>&lt;h1&gt;</kbd> для назви FAQ"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "details", "min": 3 }, "value": { "selector": "details", "min": 3 },
"message": "Create at least 3 <kbd>&lt;details&gt;</kbd> elements for the FAQ" "message": "Створіть принаймні 3 елементи <kbd>&lt;details&gt;</kbd> для FAQ"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "summary", "min": 3 }, "value": { "selector": "summary", "min": 3 },
"message": "Each <kbd>&lt;details&gt;</kbd> needs a <kbd>&lt;summary&gt;</kbd> for the question" "message": "Кожен <kbd>&lt;details&gt;</kbd> потребує <kbd>&lt;summary&gt;</kbd> для питання"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "details p", "min": 3 }, "value": { "selector": "details p", "min": 3 },
"message": "Each <kbd>&lt;details&gt;</kbd> needs a <kbd>&lt;p&gt;</kbd> for the answer" "message": "Кожен <kbd>&lt;details&gt;</kbd> потребує <kbd>&lt;p&gt;</kbd> для відповіді"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-progress-meter", "id": "html-progress-meter",
"title": "HTML Progress & Meter", "title": "HTML Progress & Meter",
"description": "Display completion status and scalar measurements natively", "description": "Відображайте статус виконання та вимірювання нативно",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "progress-basic", "id": "progress-basic",
"title": "Progress Bars", "title": "Смуги прогресу",
"description": "The <kbd>&lt;progress&gt;</kbd> element shows task completion. Use <kbd>value</kbd> for current progress and <kbd>max</kbd> for the total.<br><br><b>Note:</b> This is not a self-closing tag! Write <kbd>&lt;progress&gt;...&lt;/progress&gt;</kbd> with fallback text inside for older browsers.", "description": "Елемент <kbd>&lt;progress&gt;</kbd> показує виконання завдання. Використовуйте <kbd>value</kbd> для поточного прогресу та <kbd>max</kbd> для загального.<br><br><b>Примітка:</b> Це не самозакриваючий тег! Пишіть <kbd>&lt;progress&gt;...&lt;/progress&gt;</kbd> з резервним текстом всередині для старих браузерів.",
"task": "Create a progress bar showing 70% completion:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Download:</code><br>2. Add a <kbd>&lt;progress&gt;</kbd> with <kbd>value=\"70\"</kbd> and <kbd>max=\"100\"</kbd>", "task": "Створіть смугу прогресу, що показує 70% виконання:<br>1. Додайте <kbd>&lt;label&gt;</kbd> з текстом <code>Download:</code><br>2. Додайте <kbd>&lt;progress&gt;</kbd> з <kbd>value=\"70\"</kbd> та <kbd>max=\"100\"</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } progress { width: 100%; height: 20px; border-radius: 10px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 10px; } progress::-webkit-progress-value { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; } progress::-moz-progress-bar { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } progress { width: 100%; height: 20px; border-radius: 10px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 10px; } progress::-webkit-progress-value { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; } progress::-moz-progress-bar { background: linear-gradient(90deg, #4caf50, #8bc34a); border-radius: 10px; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,30 +21,30 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "progress", "value": "progress",
"message": "Add a <kbd>&lt;progress&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;progress&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "progress", "attr": "value", "value": "70" }, "value": { "selector": "progress", "attr": "value", "value": "70" },
"message": "Set <kbd>value=</kbd>\"70\" on the progress element" "message": "Встановіть <kbd>value=</kbd>\"70\" в елементі progress"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "progress", "attr": "max", "value": "100" }, "value": { "selector": "progress", "attr": "max", "value": "100" },
"message": "Set <kbd>max=</kbd>\"100\" on the progress element" "message": "Встановіть <kbd>max=</kbd>\"100\" в елементі progress"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "label", "value": "label",
"message": "Add a <kbd>&lt;label&gt;</kbd> for the progress bar" "message": "Додайте <kbd>&lt;label&gt;</kbd> для смуги прогресу"
} }
] ]
}, },
{ {
"id": "progress-indeterminate", "id": "progress-indeterminate",
"title": "Indeterminate Progress", "title": "Невизначений прогрес",
"description": "When progress is unknown (like loading), omit the <kbd>value</kbd> attribute. This creates an animated indeterminate state.<br><br>Useful for network requests or processes with unknown duration.", "description": "Коли прогрес невідомий (як при завантаженні), опустіть атрибут <kbd>value</kbd>. Це створює анімований невизначений стан.<br><br>Корисно для мережевих запитів або процесів з невідомою тривалістю.",
"task": "Create a loading indicator:<br>1. Add a <kbd>&lt;p&gt;</kbd> saying <code>Loading...</code><br>2. Add a <kbd>&lt;progress&gt;</kbd> without a value attribute", "task": "Створіть індикатор завантаження:<br>1. Додайте <kbd>&lt;p&gt;</kbd> з текстом <code>Loading...</code><br>2. Додайте <kbd>&lt;progress&gt;</kbd> без атрибута value",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } p { margin-bottom: 10px; color: #666; } progress { width: 100%; height: 8px; border-radius: 4px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 4px; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } p { margin-bottom: 10px; color: #666; } progress { width: 100%; height: 8px; border-radius: 4px; } progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 4px; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -55,20 +55,20 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "progress", "value": "progress",
"message": "Add a <kbd>&lt;progress&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;progress&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "p", "value": "p",
"message": "Add a <kbd>&lt;p&gt;</kbd> with loading text" "message": "Додайте <kbd>&lt;p&gt;</kbd> з текстом завантаження"
} }
] ]
}, },
{ {
"id": "meter-gauge", "id": "meter-gauge",
"title": "Meter Gauges", "title": "Шкали meter",
"description": "The <kbd>&lt;meter&gt;</kbd> element displays a scalar value within a range. Use it for measurements like disk space, battery, or ratings.<br><br>Set <kbd>low</kbd>, <kbd>high</kbd>, and <kbd>optimum</kbd> to define good/bad ranges - the browser colors it accordingly!", "description": "Елемент <kbd>&lt;meter&gt;</kbd> відображає скалярне значення в діапазоні. Використовуйте його для вимірювань, як-от місце на диску, батарея або рейтинги.<br><br>Встановіть <kbd>low</kbd>, <kbd>high</kbd> та <kbd>optimum</kbd>, щоб визначити хороші/погані діапазони - браузер забарвлює відповідно!",
"task": "Create a battery level meter:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Battery:</code><br>2. Add a <kbd>&lt;meter&gt;</kbd> with:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> and <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> and <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>", "task": "Створіть шкалу рівня батареї:<br>1. Додайте <kbd>&lt;label&gt;</kbd> з текстом <code>Battery:</code><br>2. Додайте <kbd>&lt;meter&gt;</kbd> з:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> та <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> та <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } meter { width: 100%; height: 25px; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } meter { width: 100%; height: 25px; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -79,22 +79,42 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "meter", "value": "meter",
"message": "Add a <kbd>&lt;meter&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;meter&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "meter", "attr": "value", "value": "0.8" }, "value": { "selector": "meter", "attr": "value", "value": "0.8" },
"message": "Set <kbd>value=</kbd>\"0.8\" on the meter" "message": "Встановіть <kbd>value=</kbd>\"0.8\" в meter"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "min", "value": "0" },
"message": "Встановіть <kbd>min=</kbd>\"0\" в meter"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "max", "value": "1" },
"message": "Встановіть <kbd>max=</kbd>\"1\" в meter"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "meter", "attr": "low", "value": "0.2" }, "value": { "selector": "meter", "attr": "low", "value": "0.2" },
"message": "Set <kbd>low=</kbd>\"0.2\" to define the low threshold" "message": "Встановіть <kbd>low=</kbd>\"0.2\", щоб визначити низький поріг"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "high", "value": "0.8" },
"message": "Встановіть <kbd>high=</kbd>\"0.8\", щоб визначити високий поріг"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "optimum", "value": "1" },
"message": "Встановіть <kbd>optimum=</kbd>\"1\", щоб вказати оптимальне значення"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "label", "value": "label",
"message": "Add a <kbd>&lt;label&gt;</kbd> for the meter" "message": "Додайте <kbd>&lt;label&gt;</kbd> для meter"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-datalist", "id": "html-datalist",
"title": "Datalist", "title": "Datalist",
"description": "Provide suggestions for text inputs without JavaScript", "description": "Додайте підказки для текстових полів без JavaScript",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "datalist-basic", "id": "datalist-basic",
"title": "Input with Suggestions", "title": "Поле з підказками",
"description": "The <kbd>&lt;datalist&gt;</kbd> element provides autocomplete suggestions for inputs. Connect it using the <kbd>list</kbd> attribute on the input matching the datalist's <kbd>id</kbd>.<br><br>Users can still type freely - suggestions are just helpers!", "description": "Елемент <kbd>&lt;datalist&gt;</kbd> надає підказки автозаповнення для полів. З'єднайте його за допомогою атрибута <kbd>list</kbd> на полі, що відповідає <kbd>id</kbd> datalist.<br><br>Користувачі все одно можуть вводити вільно - підказки лише помічники!",
"task": "Create a browser selector:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Browser:</code><br>2. Add an <kbd>&lt;input&gt;</kbd> with <kbd>list=\"browsers\"</kbd><br>3. Add a <kbd>&lt;datalist id=\"browsers\"&gt;</kbd> with options for Chrome, Firefox, and Safari", "task": "Створіть селектор браузера:<br>1. Додайте <kbd>&lt;label&gt;</kbd> з текстом <code>Браузер:</code><br>2. Додайте <kbd>&lt;input&gt;</kbd> з <kbd>list=\"browsers\"</kbd><br>3. Додайте <kbd>&lt;datalist id=\"browsers\"&gt;</kbd> з варіантами Chrome, Firefox та Safari",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } input { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; font-size: 16px; } input:focus { outline: 2px solid #1976d2; border-color: transparent; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } input { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; font-size: 16px; } input:focus { outline: 2px solid #1976d2; border-color: transparent; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,30 +21,30 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "datalist", "value": "datalist",
"message": "Add a <kbd>&lt;datalist&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;datalist&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input", "attr": "list", "value": "browsers" }, "value": { "selector": "input", "attr": "list", "value": "browsers" },
"message": "Connect the input to datalist using <kbd>list=</kbd>\"browsers\"" "message": "З'єднайте поле з datalist за допомогою <kbd>list=</kbd>\"browsers\""
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "option", "min": 3 }, "value": { "selector": "option", "min": 3 },
"message": "Add at least 3 <kbd>&lt;option&gt;</kbd> elements inside <kbd>&lt;datalist&gt;</kbd>" "message": "Додайте принаймні 3 елементи <kbd>&lt;option&gt;</kbd> всередину <kbd>&lt;datalist&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "label", "value": "label",
"message": "Add a <kbd>&lt;label&gt;</kbd> for the input" "message": "Додайте <kbd>&lt;label&gt;</kbd> для поля"
} }
] ]
}, },
{ {
"id": "datalist-countries", "id": "datalist-countries",
"title": "Country Selector", "title": "Селектор країн",
"description": "Datalists work great for long lists like countries. Users can type to filter suggestions instantly.<br><br>The <kbd>value</kbd> attribute is what gets entered, and you can add display text after it.", "description": "Datalist чудово працює для довгих списків, як країни. Користувачі можуть вводити, щоб миттєво фільтрувати підказки.<br><br>Атрибут <kbd>value</kbd> - це те, що вводиться, і ви можете додати текст для відображення після нього.",
"task": "Create a country input:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Country:</code><br>2. Add an <kbd>&lt;input&gt;</kbd> with <kbd>list=\"countries\"</kbd><br>3. Add a <kbd>&lt;datalist id=\"countries\"&gt;</kbd> with at least 4 country options", "task": "Створіть поле країни:<br>1. Додайте <kbd>&lt;label&gt;</kbd> з текстом <code>Країна:</code><br>2. Додайте <kbd>&lt;input&gt;</kbd> з <kbd>list=\"countries\"</kbd><br>3. Додайте <kbd>&lt;datalist id=\"countries\"&gt;</kbd> з принаймні 4 варіантами країн",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 30px; background: linear-gradient(135deg, #e0f7fa 0%, #b2ebf2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } label { display: block; margin-bottom: 10px; font-weight: 600; color: #00695c; } input { width: 100%; padding: 12px 15px; border: 2px solid #26a69a; border-radius: 8px; font-size: 16px; background: white; } input:focus { outline: none; border-color: #00695c; box-shadow: 0 0 0 3px rgba(38,166,154,0.2); }", "previewBaseCSS": "body { font-family: system-ui; padding: 30px; background: linear-gradient(135deg, #e0f7fa 0%, #b2ebf2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } label { display: block; margin-bottom: 10px; font-weight: 600; color: #00695c; } input { width: 100%; padding: 12px 15px; border: 2px solid #26a69a; border-radius: 8px; font-size: 16px; background: white; } input:focus { outline: none; border-color: #00695c; box-shadow: 0 0 0 3px rgba(38,166,154,0.2); }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -55,22 +55,22 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "datalist", "value": "datalist",
"message": "Add a <kbd>&lt;datalist&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;datalist&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "datalist", "attr": "id", "value": "countries" }, "value": { "selector": "datalist", "attr": "id", "value": "countries" },
"message": "Set <kbd>id=</kbd>\"countries\" on the datalist" "message": "Встановіть <kbd>id=</kbd>\"countries\" в datalist"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "input", "attr": "list", "value": "countries" }, "value": { "selector": "input", "attr": "list", "value": "countries" },
"message": "Connect the input using <kbd>list=</kbd>\"countries\"" "message": "З'єднайте поле за допомогою <kbd>list=</kbd>\"countries\""
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "option", "min": 4 }, "value": { "selector": "option", "min": 4 },
"message": "Add at least 4 country options" "message": "Додайте принаймні 4 варіанти країн"
} }
] ]
} }

View File

@@ -1,16 +1,16 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-dialog", "id": "html-dialog",
"title": "Dialogs", "title": "Діалоги",
"description": "Create modal dialogs without JavaScript libraries", "description": "Створюйте модальні діалоги без JavaScript бібліотек",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "dialog-basic", "id": "dialog-basic",
"title": "Open Dialog", "title": "Відкрити діалог",
"description": "The <kbd>&lt;dialog&gt;</kbd> element creates a native modal. Add the <kbd>open</kbd> attribute to show it.<br><br>Use <kbd>&lt;form method=\"dialog\"&gt;</kbd> inside to close it when the form submits - no JavaScript needed!", "description": "Елемент <kbd>&lt;dialog&gt;</kbd> створює нативне модальне вікно. Додайте атрибут <kbd>open</kbd>, щоб показати його.<br><br>Використовуйте <kbd>&lt;form method=\"dialog\"&gt;</kbd> всередині, щоб закрити його при відправці форми - без JavaScript!",
"task": "Create a dialog with:<br>1. The <kbd>open</kbd> attribute to show it<br>2. An <kbd>&lt;h2&gt;</kbd> saying <code>Welcome!</code><br>3. A <kbd>&lt;p&gt;</kbd> with a greeting message<br>4. A <kbd>&lt;form method=\"dialog\"&gt;</kbd> with a close button", "task": "Створіть діалог з:<br>1. Атрибутом <kbd>open</kbd> для відображення<br>2. Елементом <kbd>&lt;h2&gt;</kbd> з текстом <code>Вітаємо!</code><br>3. Елементом <kbd>&lt;p&gt;</kbd> з привітальним повідомленням<br>4. Елементом <kbd>&lt;form method=\"dialog\"&gt;</kbd> з кнопкою закриття",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; min-height: 200px; background: #f5f5f5; } dialog { border: none; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 25px; max-width: 400px; } dialog::backdrop { background: rgba(0,0,0,0.5); } dialog h2 { margin: 0 0 15px 0; color: #1976d2; } dialog p { color: #666; margin: 0 0 20px 0; } dialog button { background: #1976d2; color: white; border: none; padding: 10px 25px; border-radius: 6px; cursor: pointer; font-size: 16px; } dialog button:hover { background: #1565c0; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; min-height: 200px; background: #f5f5f5; } dialog { border: none; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 25px; max-width: 400px; } dialog::backdrop { background: rgba(0,0,0,0.5); } dialog h2 { margin: 0 0 15px 0; color: #1976d2; } dialog p { color: #666; margin: 0 0 20px 0; } dialog button { background: #1976d2; color: white; border: none; padding: 10px 25px; border-radius: 6px; cursor: pointer; font-size: 16px; } dialog button:hover { background: #1565c0; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,35 +21,35 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog", "value": "dialog",
"message": "Add a <kbd>&lt;dialog&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;dialog&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "dialog", "attr": "open", "value": true }, "value": { "selector": "dialog", "attr": "open", "value": true },
"message": "Add the <kbd>open</kbd> attribute to show the dialog" "message": "Додайте атрибут <kbd>open</kbd> щоб показати діалог"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog h2", "value": "dialog h2",
"message": "Add an <kbd>&lt;h2&gt;</kbd> heading inside the dialog" "message": "Додайте заголовок <kbd>&lt;h2&gt;</kbd> всередині діалогу"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "form[method='dialog']", "value": "form[method='dialog']",
"message": "Add a <kbd>&lt;form method=\"dialog\"&gt;</kbd> for closing" "message": "Додайте <kbd>&lt;form method=\"dialog\"&gt;</kbd> для закриття"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog button", "value": "dialog button",
"message": "Add a close button inside the form" "message": "Додайте кнопку закриття всередині форми"
} }
] ]
}, },
{ {
"id": "dialog-form", "id": "dialog-form",
"title": "Dialog + Form", "title": "Діалог + Форма",
"description": "Dialogs can contain full forms. The <kbd>method=\"dialog\"</kbd> makes the form close the dialog on submit instead of sending data.<br><br>This pattern is perfect for confirmation dialogs, quick inputs, or settings panels.", "description": "Діалоги можуть містити повні форми. <kbd>method=\"dialog\"</kbd> змушує форму закривати діалог при відправці замість відправки даних.<br><br>Цей шаблон ідеальний для діалогів підтвердження, швидкого введення або панелей налаштувань.",
"task": "Create a confirmation dialog:<br>1. Add <kbd>open</kbd> to show it<br>2. An <kbd>&lt;h2&gt;</kbd> saying <code>Confirm Delete</code><br>3. A <kbd>&lt;p&gt;</kbd> asking <code>Are you sure?</code><br>4. A <kbd>&lt;form method=\"dialog\"&gt;</kbd> with Cancel and Delete buttons", "task": "Створіть діалог підтвердження:<br>1. Додайте <kbd>open</kbd> для відображення<br>2. Елемент <kbd>&lt;h2&gt;</kbd> з текстом <code>Підтвердити видалення</code><br>3. Елемент <kbd>&lt;p&gt;</kbd> з питанням <code>Ви впевнені?</code><br>4. Елемент <kbd>&lt;form method=\"dialog\"&gt;</kbd> з кнопками Скасувати та Видалити",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; min-height: 200px; background: linear-gradient(135deg, #fce4ec 0%, #f8bbd9 100%); } dialog { border: none; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 25px; max-width: 350px; text-align: center; } dialog h2 { margin: 0 0 10px 0; color: #c62828; } dialog p { color: #666; margin: 0 0 20px 0; } dialog form { display: flex; gap: 10px; justify-content: center; } dialog button { padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; border: none; } dialog button:first-child { background: #e0e0e0; color: #333; } dialog button:last-child { background: #c62828; color: white; } dialog button:hover { opacity: 0.9; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; min-height: 200px; background: linear-gradient(135deg, #fce4ec 0%, #f8bbd9 100%); } dialog { border: none; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); padding: 25px; max-width: 350px; text-align: center; } dialog h2 { margin: 0 0 10px 0; color: #c62828; } dialog p { color: #666; margin: 0 0 20px 0; } dialog form { display: flex; gap: 10px; justify-content: center; } dialog button { padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; border: none; } dialog button:first-child { background: #e0e0e0; color: #333; } dialog button:last-child { background: #c62828; color: white; } dialog button:hover { opacity: 0.9; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -60,22 +60,22 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog[open]", "value": "dialog[open]",
"message": "Add a <kbd>&lt;dialog&gt;</kbd> with the open attribute" "message": "Додайте <kbd>&lt;dialog&gt;</kbd> з атрибутом open"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "dialog h2", "value": "dialog h2",
"message": "Add a heading to the dialog" "message": "Додайте заголовок до діалогу"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "form[method='dialog']", "value": "form[method='dialog']",
"message": "Add a <kbd>&lt;form method=\"dialog\"&gt;</kbd> for the buttons" "message": "Додайте <kbd>&lt;form method=\"dialog\"&gt;</kbd> для кнопок"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "dialog button", "min": 2 }, "value": { "selector": "dialog button", "min": 2 },
"message": "Add at least 2 buttons (Cancel and Confirm)" "message": "Додайте принаймні 2 кнопки (Скасувати та Підтвердити)"
} }
] ]
} }

View File

@@ -1,16 +1,16 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-fieldset", "id": "html-forms-fieldset",
"title": "Fieldsets", "title": "Fieldset",
"description": "Group form controls with fieldset and legend elements", "description": "Групуйте елементи форми за допомогою fieldset та legend",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "fieldset-basic", "id": "fieldset-basic",
"title": "Grouping with Fieldset", "title": "Групування з Fieldset",
"description": "The <kbd>&lt;fieldset&gt;</kbd> element groups related form controls together. Add a <kbd>&lt;legend&gt;</kbd> as the first child to give the group a title.<br><br>This helps with accessibility and visual organization of complex forms.", "description": "Елемент <kbd>&lt;fieldset&gt;</kbd> групує пов'язані елементи форми разом. Додайте <kbd>&lt;legend&gt;</kbd> як перший дочірній елемент, щоб дати групі заголовок.<br><br>Це допомагає з доступністю та візуальною організацією складних форм.",
"task": "Create a form with a fieldset:<br>1. A <kbd>&lt;form&gt;</kbd> element<br>2. A <kbd>&lt;fieldset&gt;</kbd> inside<br>3. A <kbd>&lt;legend&gt;</kbd> saying <code>Personal Info</code><br>4. Two labeled inputs for name and email", "task": "Створіть форму з fieldset:<br>1. Елемент <kbd>&lt;form&gt;</kbd><br>2. Елемент <kbd>&lt;fieldset&gt;</kbd> всередині<br>3. Елемент <kbd>&lt;legend&gt;</kbd> з текстом <code>Особисті дані</code><br>4. Два підписаних поля для імені та email",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f0f4f8; } form { max-width: 400px; } fieldset { border: 2px solid #3498db; border-radius: 10px; padding: 20px; background: white; } legend { color: #3498db; font-weight: 600; padding: 0 10px; font-size: 1.1rem; } label { display: block; margin: 15px 0 5px; color: #333; font-weight: 500; } input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; } input:focus { outline: 2px solid #3498db; border-color: transparent; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f0f4f8; } form { max-width: 400px; } fieldset { border: 2px solid #3498db; border-radius: 10px; padding: 20px; background: white; } legend { color: #3498db; font-weight: 600; padding: 0 10px; font-size: 1.1rem; } label { display: block; margin: 15px 0 5px; color: #333; font-weight: 500; } input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; } input:focus { outline: 2px solid #3498db; border-color: transparent; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,35 +21,35 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "form", "value": "form",
"message": "Add a <kbd>&lt;form&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;form&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "fieldset", "value": "fieldset",
"message": "Add a <kbd>&lt;fieldset&gt;</kbd> inside the form" "message": "Додайте <kbd>&lt;fieldset&gt;</kbd> всередину форми"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "legend", "value": "legend",
"message": "Add a <kbd>&lt;legend&gt;</kbd> to title your fieldset" "message": "Додайте <kbd>&lt;legend&gt;</kbd> як заголовок fieldset"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "label", "min": 2 }, "value": { "selector": "label", "min": 2 },
"message": "Add at least 2 labels" "message": "Додайте принаймні 2 мітки"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "input", "min": 2 }, "value": { "selector": "input", "min": 2 },
"message": "Add at least 2 input fields" "message": "Додайте принаймні 2 поля введення"
} }
] ]
}, },
{ {
"id": "fieldset-textarea", "id": "fieldset-textarea",
"title": "Adding Textarea", "title": "Додавання Textarea",
"description": "The <kbd>&lt;textarea&gt;</kbd> element creates a multi-line text input, perfect for longer content like messages or descriptions.<br><br>Use <kbd>rows</kbd> and <kbd>cols</kbd> attributes to set default size.", "description": "Елемент <kbd>&lt;textarea&gt;</kbd> створює багаторядкове текстове поле, ідеальне для довшого вмісту як повідомлення або описи.<br><br>Використовуйте атрибути <kbd>rows</kbd> та <kbd>cols</kbd> для встановлення розміру за замовчуванням.",
"task": "Create a contact form:<br>1. A <kbd>&lt;fieldset&gt;</kbd> with <kbd>&lt;legend&gt;</kbd> <code>Contact Us</code><br>2. A labeled <kbd>&lt;input&gt;</kbd> for email<br>3. A labeled <kbd>&lt;textarea&gt;</kbd> for the message<br>4. A submit <kbd>&lt;button&gt;</kbd>", "task": "Створіть контактну форму:<br>1. Елемент <kbd>&lt;fieldset&gt;</kbd> з <kbd>&lt;legend&gt;</kbd> <code>Зв'яжіться з нами</code><br>2. Підписане <kbd>&lt;input&gt;</kbd> для email<br>3. Підписане <kbd>&lt;textarea&gt;</kbd> для повідомлення<br>4. Кнопка <kbd>&lt;button&gt;</kbd> відправки",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } form { max-width: 450px; margin: 0 auto; } fieldset { border: none; border-radius: 15px; padding: 30px; background: white; box-shadow: 0 10px 40px rgba(0,0,0,0.2); } legend { color: #667eea; font-weight: 700; padding: 0; font-size: 1.5rem; margin-bottom: 10px; } label { display: block; margin: 20px 0 8px; color: #333; font-weight: 500; } input, textarea { width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 16px; box-sizing: border-box; font-family: inherit; } input:focus, textarea:focus { outline: none; border-color: #667eea; } textarea { resize: vertical; min-height: 100px; } button { margin-top: 20px; width: 100%; padding: 14px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; } button:hover { opacity: 0.9; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } form { max-width: 450px; margin: 0 auto; } fieldset { border: none; border-radius: 15px; padding: 30px; background: white; box-shadow: 0 10px 40px rgba(0,0,0,0.2); } legend { color: #667eea; font-weight: 700; padding: 0; font-size: 1.5rem; margin-bottom: 10px; } label { display: block; margin: 20px 0 8px; color: #333; font-weight: 500; } input, textarea { width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 16px; box-sizing: border-box; font-family: inherit; } input:focus, textarea:focus { outline: none; border-color: #667eea; } textarea { resize: vertical; min-height: 100px; } button { margin-top: 20px; width: 100%; padding: 14px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; } button:hover { opacity: 0.9; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -60,35 +60,35 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "fieldset", "value": "fieldset",
"message": "Add a <kbd>&lt;fieldset&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;fieldset&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "legend", "value": "legend",
"message": "Add a <kbd>&lt;legend&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;legend&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "textarea", "value": "textarea",
"message": "Add a <kbd>&lt;textarea&gt;</kbd> for the message" "message": "Додайте <kbd>&lt;textarea&gt;</kbd> для повідомлення"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "button", "value": "button",
"message": "Add a submit button" "message": "Додайте кнопку відправки"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "input", "value": "input",
"message": "Add an input field for email" "message": "Додайте поле для email"
} }
] ]
}, },
{ {
"id": "fieldset-multiple", "id": "fieldset-multiple",
"title": "Multiple Fieldsets", "title": "Кілька Fieldsetів",
"description": "Complex forms can use multiple <kbd>&lt;fieldset&gt;</kbd> elements to organize different sections.<br><br>This improves usability for long forms like registration or checkout pages.", "description": "Складні форми можуть використовувати кілька елементів <kbd>&lt;fieldset&gt;</kbd> для організації різних секцій.<br><br>Це покращує зручність використання довгих форм як реєстрація або оформлення замовлення.",
"task": "Create a registration form with 2 fieldsets:<br>1. <code>Account Info</code> with username and password inputs<br>2. <code>Preferences</code> with a textarea for bio<br>3. A submit button outside the fieldsets", "task": "Створіть форму реєстрації з 2 fieldsetами:<br>1. <code>Дані облікового запису</code> з полями ім'я користувача та пароль<br>2. <code>Налаштування</code> з textarea для біо<br>3. Кнопка відправки поза fieldsetами",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #fafafa; } form { max-width: 500px; } fieldset { border: 1px solid #ddd; border-radius: 10px; padding: 20px; margin-bottom: 20px; background: white; } legend { color: #2c3e50; font-weight: 600; padding: 0 10px; } label { display: block; margin: 15px 0 5px; color: #555; } input, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; font-family: inherit; } input:focus, textarea:focus { outline: 2px solid #3498db; border-color: transparent; } textarea { resize: vertical; min-height: 80px; } button { width: 100%; padding: 14px; background: #2c3e50; color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; } button:hover { background: #34495e; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #fafafa; } form { max-width: 500px; } fieldset { border: 1px solid #ddd; border-radius: 10px; padding: 20px; margin-bottom: 20px; background: white; } legend { color: #2c3e50; font-weight: 600; padding: 0 10px; } label { display: block; margin: 15px 0 5px; color: #555; } input, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; font-family: inherit; } input:focus, textarea:focus { outline: 2px solid #3498db; border-color: transparent; } textarea { resize: vertical; min-height: 80px; } button { width: 100%; padding: 14px; background: #2c3e50; color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; } button:hover { background: #34495e; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -99,27 +99,27 @@
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "fieldset", "min": 2 }, "value": { "selector": "fieldset", "min": 2 },
"message": "Create at least 2 fieldsets" "message": "Створіть принаймні 2 fieldseti"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "legend", "min": 2 }, "value": { "selector": "legend", "min": 2 },
"message": "Add a legend to each fieldset" "message": "Додайте legend до кожного fieldseta"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "textarea", "value": "textarea",
"message": "Add a textarea for the bio" "message": "Додайте textarea для біо"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "button", "value": "button",
"message": "Add a submit button" "message": "Додайте кнопку відправки"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "input", "min": 2 }, "value": { "selector": "input", "min": 2 },
"message": "Add at least 2 input fields" "message": "Додайте принаймні 2 поля введення"
} }
] ]
} }

View File

@@ -1,125 +1,42 @@
{ {
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-tables", "id": "html-tables",
"title": "HTML Tables", "title": "Таблиці HTML",
"description": "Create structured data tables with headers and captions", "description": "Створюйте структуровані таблиці даних із семантичною розміткою",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "table-basic", "id": "table-basic",
"title": "Basic Table Structure", "title": "Таблиці даних",
"description": "Tables use <kbd>&lt;table&gt;</kbd> with <kbd>&lt;tr&gt;</kbd> for rows. Inside rows, use <kbd>&lt;th&gt;</kbd> for headers and <kbd>&lt;td&gt;</kbd> for data cells.<br><br>The <kbd>&lt;caption&gt;</kbd> element provides an accessible title for the table.", "description": "Таблиці відображають структуровані дані в рядках і стовпцях. Використовуйте <kbd>&lt;table&gt;</kbd> як контейнер, <kbd>&lt;tr&gt;</kbd> для рядків, <kbd>&lt;th&gt;</kbd> для комірок заголовка та <kbd>&lt;td&gt;</kbd> для комірок даних.<br><br>Додайте <kbd>&lt;caption&gt;</kbd> для доступного заголовка, що описує вміст таблиці.",
"task": "Create a simple table with:<br>1. A <kbd>&lt;caption&gt;</kbd> saying <code>Fruit Prices</code><br>2. A header row with <code>Fruit</code> and <code>Price</code> columns<br>3. At least 2 data rows", "task": "Створіть таблицю цін:<br>1. Елемент <kbd>&lt;caption&gt;</kbd> з текстом <code>Pricing</code><br>2. Рядок заголовка з <code>Plan</code> та <code>Price</code><br>3. Два рядки даних для Basic ($9) та Pro ($29)",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px; font-weight: 600; font-size: 1.2rem; color: #333; background: #f8f9fa; } th, td { padding: 12px 20px; text-align: left; border-bottom: 1px solid #eee; } th { background: #3498db; color: white; font-weight: 500; } tr:hover { background: #f8f9fa; } tr:last-child td { border-bottom: none; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; } table { border-collapse: collapse; width: 100%; max-width: 350px; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 16px; font-weight: 600; font-size: 1.1rem; color: #333; background: #f8f9fa; } th, td { padding: 14px 20px; text-align: left; border-bottom: 1px solid #eee; } th { background: steelblue; color: white; font-weight: 500; } tr:last-child td { border-bottom: none; } tr:hover td { background: #f8f9fa; }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<table>\n <caption>Fruit Prices</caption>\n <tr>\n <th>Fruit</th>\n <th>Price</th>\n </tr>\n <tr>\n <td>Apple</td>\n <td>$1.50</td>\n </tr>\n <tr>\n <td>Banana</td>\n <td>$0.75</td>\n </tr>\n</table>", "solution": "<table>\n <caption>Pricing</caption>\n <tr>\n <th>Plan</th>\n <th>Price</th>\n </tr>\n <tr>\n <td>Basic</td>\n <td>$9</td>\n </tr>\n <tr>\n <td>Pro</td>\n <td>$29</td>\n </tr>\n</table>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "table", "value": "table",
"message": "Add a <kbd>&lt;table&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;table&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "caption", "value": "caption",
"message": "Add a <kbd>&lt;caption&gt;</kbd> for the table title" "message": "Додайте <kbd>&lt;caption&gt;</kbd> для заголовка таблиці"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "th", "min": 2 }, "value": { "selector": "th", "min": 2 },
"message": "Add at least 2 header cells (th)" "message": "Додайте комірки заголовка (<kbd>&lt;th&gt;</kbd>) для Plan та Price"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "tr", "min": 3 }, "value": { "selector": "tr", "min": 3 },
"message": "Add at least 3 rows (1 header + 2 data rows)" "message": "Додайте 3 рядки (1 заголовок + 2 рядки даних)"
}
]
},
{
"id": "table-thead-tbody",
"title": "Table Head & Body",
"description": "Use <kbd>&lt;thead&gt;</kbd> to group header rows and <kbd>&lt;tbody&gt;</kbd> to group data rows. This helps browsers and assistive technology understand the table structure.<br><br>You can also use <kbd>&lt;tfoot&gt;</kbd> for footer rows like totals.",
"task": "Create a structured table:<br>1. A <kbd>&lt;caption&gt;</kbd> with <code>Monthly Sales</code><br>2. A <kbd>&lt;thead&gt;</kbd> with Month and Revenue headers<br>3. A <kbd>&lt;tbody&gt;</kbd> with at least 2 data rows",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; box-sizing: border-box; } table { border-collapse: collapse; width: 100%; max-width: 450px; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.2); } caption { padding: 20px; font-weight: 700; font-size: 1.3rem; color: white; background: transparent; text-shadow: 0 2px 4px rgba(0,0,0,0.2); caption-side: top; } thead { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } th { padding: 15px 20px; text-align: left; color: white; font-weight: 500; } tbody tr { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } td { padding: 15px 20px; color: #333; } tbody tr:last-child { border-bottom: none; }",
"sandboxCSS": "",
"initialCode": "",
"solution": "<table>\n <caption>Monthly Sales</caption>\n <thead>\n <tr>\n <th>Month</th>\n <th>Revenue</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>January</td>\n <td>$12,500</td>\n </tr>\n <tr>\n <td>February</td>\n <td>$14,200</td>\n </tr>\n </tbody>\n</table>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "table",
"message": "Add a <kbd>&lt;table&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "caption",
"message": "Add a <kbd>&lt;caption&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "thead",
"message": "Add a <kbd>&lt;thead&gt;</kbd> for the header section"
},
{
"type": "element_exists",
"value": "tbody",
"message": "Add a <kbd>&lt;tbody&gt;</kbd> for the data rows"
},
{
"type": "element_count",
"value": { "selector": "tbody tr", "min": 2 },
"message": "Add at least 2 data rows in tbody"
}
]
},
{
"id": "table-complete",
"title": "Complete Table with Footer",
"description": "Add <kbd>&lt;tfoot&gt;</kbd> to create a footer section for totals or summary data. The footer stays at the bottom even if tbody has many rows.<br><br>Combine all sections for a fully structured, accessible table.",
"task": "Create a complete table:<br>1. A <kbd>&lt;caption&gt;</kbd> with <code>Order Summary</code><br>2. A <kbd>&lt;thead&gt;</kbd> with Item and Price headers<br>3. A <kbd>&lt;tbody&gt;</kbd> with 2 items<br>4. A <kbd>&lt;tfoot&gt;</kbd> with a Total row",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #fafafa; } table { border-collapse: collapse; width: 100%; max-width: 400px; background: white; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } caption { padding: 15px 20px; font-weight: 600; font-size: 1.1rem; color: #333; text-align: left; background: #f8f9fa; border-bottom: 2px solid #eee; } th, td { padding: 12px 20px; text-align: left; } thead th { background: #2c3e50; color: white; } tbody td { border-bottom: 1px solid #eee; } tbody tr:hover { background: #f8f9fa; } tfoot { background: #ecf0f1; font-weight: 600; } tfoot td { border-top: 2px solid #2c3e50; color: #2c3e50; }",
"sandboxCSS": "",
"initialCode": "",
"solution": "<table>\n <caption>Order Summary</caption>\n <thead>\n <tr>\n <th>Item</th>\n <th>Price</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>Widget</td>\n <td>$25.00</td>\n </tr>\n <tr>\n <td>Gadget</td>\n <td>$35.00</td>\n </tr>\n </tbody>\n <tfoot>\n <tr>\n <td>Total</td>\n <td>$60.00</td>\n </tr>\n </tfoot>\n</table>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "table",
"message": "Add a <kbd>&lt;table&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "caption",
"message": "Add a <kbd>&lt;caption&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "thead",
"message": "Add a <kbd>&lt;thead&gt;</kbd> section"
},
{
"type": "element_exists",
"value": "tbody",
"message": "Add a <kbd>&lt;tbody&gt;</kbd> section"
},
{
"type": "element_exists",
"value": "tfoot",
"message": "Add a <kbd>&lt;tfoot&gt;</kbd> section for the total"
},
{
"type": "element_count",
"value": { "selector": "tbody tr", "min": 2 },
"message": "Add at least 2 item rows in tbody"
} }
] ]
} }

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-marquee", "id": "html-marquee",
"title": "HTML Marquee", "title": "HTML Marquee",
"description": "Create scrolling text with the classic (deprecated but fun!) marquee element", "description": "Створюйте біжучий текст за допомогою класичного (застарілого але веселого!) елемента marquee",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "marquee-basic", "id": "marquee-basic",
"title": "Scrolling Text", "title": "Біжучий текст",
"description": "The <kbd>&lt;marquee&gt;</kbd> element creates scrolling text - a classic from the early web! While deprecated, it still works in most browsers.<br><br>Note: For production, use CSS animations instead. But for learning and fun, marquee is great!", "description": "Елемент <kbd>&lt;marquee&gt;</kbd> створює біжучий текст - класика раннього вебу! Хоча він застарілий, все ще працює в більшості браузерів.<br><br>Примітка: Для продакшену використовуйте CSS-анімації. Але для навчання та розваги marquee чудовий!",
"task": "Create a simple marquee:<br>1. Add a <kbd>&lt;marquee&gt;</kbd> element<br>2. Put some text inside like <code>Welcome to my website!</code>", "task": "Створіть простий marquee:<br>1. Додайте елемент <kbd>&lt;marquee&gt;</kbd><br>2. Помістіть текст всередину, наприклад <code>Ласкаво просимо на мій сайт!</code>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2rem; color: #00ff00; text-shadow: 0 0 10px #00ff00, 0 0 20px #00ff00; font-family: 'Courier New', monospace; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2rem; color: #00ff00; text-shadow: 0 0 10px #00ff00, 0 0 20px #00ff00; font-family: 'Courier New', monospace; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -21,15 +21,15 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "marquee", "value": "marquee",
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;marquee&gt;</kbd>"
} }
] ]
}, },
{ {
"id": "marquee-direction", "id": "marquee-direction",
"title": "Direction & Behavior", "title": "Напрямок та поведінка",
"description": "Control the marquee with attributes:<br>• <kbd>direction</kbd>: left, right, up, down<br>• <kbd>behavior</kbd>: scroll (default), slide (stops at edge), alternate (bounces)<br>• <kbd>scrollamount</kbd>: speed (default is 6)", "description": "Керуйте marquee за допомогою атрибутів:<br>• <kbd>direction</kbd>: left, right, up, down<br>• <kbd>behavior</kbd>: scroll (за замовчуванням), slide (зупиняється на краю), alternate (відбивається)<br>• <kbd>scrollamount</kbd>: швидкість (за замовчуванням 6)",
"task": "Create a bouncing marquee:<br>1. Add a <kbd>&lt;marquee&gt;</kbd> element<br>2. Set <kbd>behavior=\"alternate\"</kbd> to make it bounce<br>3. Add some fun text", "task": "Створіть marquee що відбивається:<br>1. Додайте елемент <kbd>&lt;marquee&gt;</kbd><br>2. Встановіть <kbd>behavior=\"alternate\"</kbd> щоб він відбивався<br>3. Додайте веселий текст",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2.5rem; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); font-weight: bold; }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); min-height: 150px; display: flex; align-items: center; } marquee { font-size: 2.5rem; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); font-weight: bold; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -40,20 +40,20 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "marquee", "value": "marquee",
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;marquee&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "marquee", "attr": "behavior", "value": "alternate" }, "value": { "selector": "marquee", "attr": "behavior", "value": "alternate" },
"message": "Add <kbd>behavior=</kbd>\"alternate\" to make it bounce" "message": "Додайте <kbd>behavior=</kbd>\"alternate\" щоб він відбивався"
} }
] ]
}, },
{ {
"id": "marquee-retro", "id": "marquee-retro",
"title": "Retro News Ticker", "title": "Ретро-стрічка новин",
"description": "Combine multiple marquee attributes for a classic news ticker effect. You can even put multiple elements inside!<br><br>Remember: This is deprecated HTML. Modern sites use CSS animations, but marquee is great for understanding web history.", "description": "Поєднайте кілька атрибутів marquee для класичного ефекту стрічки новин. Ви навіть можете помістити кілька елементів всередину!<br><br>Пам'ятайте: Це застарілий HTML. Сучасні сайти використовують CSS-анімації, але marquee чудовий для розуміння історії вебу.",
"task": "Create a news ticker:<br>1. A <kbd>&lt;marquee&gt;</kbd> with <kbd>direction=\"left\"</kbd><br>2. Set <kbd>scrollamount=\"5\"</kbd> for smooth scrolling<br>3. Add a breaking news headline inside", "task": "Створіть стрічку новин:<br>1. Елемент <kbd>&lt;marquee&gt;</kbd> з <kbd>direction=\"left\"</kbd><br>2. Встановіть <kbd>scrollamount=\"5\"</kbd> для плавного прокручування<br>3. Додайте всередину заголовок термінових новин",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 0; margin: 0; background: #1a1a2e; } marquee { background: linear-gradient(90deg, #c0392b 0%, #e74c3c 50%, #c0392b 100%); padding: 15px 0; font-size: 1.3rem; color: white; font-weight: 500; text-transform: uppercase; letter-spacing: 1px; border-top: 3px solid #f1c40f; border-bottom: 3px solid #f1c40f; }", "previewBaseCSS": "body { font-family: system-ui; padding: 0; margin: 0; background: #1a1a2e; } marquee { background: linear-gradient(90deg, #c0392b 0%, #e74c3c 50%, #c0392b 100%); padding: 15px 0; font-size: 1.3rem; color: white; font-weight: 500; text-transform: uppercase; letter-spacing: 1px; border-top: 3px solid #f1c40f; border-bottom: 3px solid #f1c40f; }",
"sandboxCSS": "", "sandboxCSS": "",
@@ -64,17 +64,17 @@
{ {
"type": "element_exists", "type": "element_exists",
"value": "marquee", "value": "marquee",
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;marquee&gt;</kbd>"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "marquee", "attr": "direction", "value": "left" }, "value": { "selector": "marquee", "attr": "direction", "value": "left" },
"message": "Add <kbd>direction=</kbd>\"left\" for horizontal scrolling" "message": "Додайте <kbd>direction=</kbd>\"left\" для горизонтального прокручування"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "marquee", "attr": "scrollamount", "value": "5" }, "value": { "selector": "marquee", "attr": "scrollamount", "value": "5" },
"message": "Add <kbd>scrollamount=</kbd>\"5\" for smooth speed" "message": "Додайте <kbd>scrollamount=</kbd>\"5\" для плавної швидкості"
} }
] ]
} }

View File

@@ -2,99 +2,169 @@
"$schema": "../../schemas/code-crispies-module-schema.json", "$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-svg", "id": "html-svg",
"title": "HTML SVG", "title": "HTML SVG",
"description": "Draw scalable vector graphics directly in HTML", "description": "Малюйте масштабовану векторну графіку безпосередньо в HTML",
"mode": "html", "mode": "html",
"difficulty": "beginner", "difficulty": "beginner",
"lessons": [ "lessons": [
{ {
"id": "svg-circle", "id": "svg-circle",
"title": "Drawing Circles", "title": "Малювання кіл",
"description": "SVG (Scalable Vector Graphics) lets you draw shapes directly in HTML. The <kbd>&lt;svg&gt;</kbd> element is the container, with <kbd>width</kbd> and <kbd>height</kbd> attributes.<br><br>Use <kbd>&lt;circle&gt;</kbd> with <kbd>cx</kbd>, <kbd>cy</kbd> (center) and <kbd>r</kbd> (radius) to draw circles.", "description": "SVG (Scalable Vector Graphics) дозволяє малювати фігури безпосередньо в HTML. Елемент <kbd>&lt;svg&gt;</kbd> є контейнером з атрибутами <kbd>width</kbd> та <kbd>height</kbd>.<br><br>Використовуйте <kbd>&lt;circle&gt;</kbd> з <kbd>cx</kbd>, <kbd>cy</kbd> (центр) та <kbd>r</kbd> (радіус) для малювання кіл.",
"task": "Create an SVG with a circle:<br>1. An <kbd>&lt;svg&gt;</kbd> with width=\"200\" and height=\"200\"<br>2. A <kbd>&lt;circle&gt;</kbd> centered at (100,100) with radius 50<br>3. Add a <kbd>fill</kbd> color", "task": "Створіть SVG з колом:<br>1. Елемент <kbd>&lt;svg&gt;</kbd> з width=\"200\" та height=\"200\"<br>2. Елемент <kbd>&lt;circle&gt;</kbd> з центром у (100,100) та радіусом 50<br>3. Додайте колір <kbd>fill</kbd>",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; display: flex; justify-content: center; } svg { background: white; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: #f5f5f5; display: flex; justify-content: center; } svg { background: white; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"#3498db\" />\n</svg>", "solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"steelblue\" />\n</svg>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "svg", "value": "svg",
"message": "Add an <kbd>&lt;svg&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;svg&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "circle", "value": "circle",
"message": "Add a <kbd>&lt;circle&gt;</kbd> element inside the SVG" "message": "Додайте елемент <kbd>&lt;circle&gt;</kbd> всередину SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "width", "value": "200" },
"message": "Встановіть <kbd>width=</kbd>\"200\" в SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "height", "value": "200" },
"message": "Встановіть <kbd>height=</kbd>\"200\" в SVG"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "circle", "attr": "cx", "value": "100" }, "value": { "selector": "circle", "attr": "cx", "value": "100" },
"message": "Set <kbd>cx=</kbd>\"100\" for the circle's horizontal center" "message": "Встановіть <kbd>cx=</kbd>\"100\" для горизонтального центру кола"
}, },
{ {
"type": "attribute_value", "type": "attribute_value",
"value": { "selector": "circle", "attr": "cy", "value": "100" }, "value": { "selector": "circle", "attr": "cy", "value": "100" },
"message": "Set <kbd>cy=</kbd>\"100\" for the circle's vertical center" "message": "Встановіть <kbd>cy=</kbd>\"100\" для вертикального центру кола"
},
{
"type": "attribute_value",
"value": { "selector": "circle", "attr": "r", "value": "50" },
"message": "Встановіть <kbd>r=</kbd>\"50\" для радіуса кола"
} }
] ]
}, },
{ {
"id": "svg-rect-line", "id": "svg-rect-line",
"title": "Rectangles & Lines", "title": "Прямокутники та лінії",
"description": "Draw rectangles with <kbd>&lt;rect&gt;</kbd> using <kbd>x</kbd>, <kbd>y</kbd>, <kbd>width</kbd>, <kbd>height</kbd>.<br><br>Draw lines with <kbd>&lt;line&gt;</kbd> using <kbd>x1</kbd>, <kbd>y1</kbd> (start) and <kbd>x2</kbd>, <kbd>y2</kbd> (end). Lines need a <kbd>stroke</kbd> color!", "description": "Малюйте прямокутники за допомогою <kbd>&lt;rect&gt;</kbd> використовуючи <kbd>x</kbd>, <kbd>y</kbd>, <kbd>width</kbd>, <kbd>height</kbd>.<br><br>Малюйте лінії за допомогою <kbd>&lt;line&gt;</kbd> використовуючи <kbd>x1</kbd>, <kbd>y1</kbd> (початок) та <kbd>x2</kbd>, <kbd>y2</kbd> (кінець). Лінії потребують колір <kbd>stroke</kbd>!",
"task": "Create an SVG with:<br>1. An <kbd>&lt;svg&gt;</kbd> (200x150)<br>2. A <kbd>&lt;rect&gt;</kbd> at position (20,20) with size 80x60<br>3. A <kbd>&lt;line&gt;</kbd> from (120,30) to (180,90) with a stroke color", "task": "Створіть SVG з:<br>1. Елементом <kbd>&lt;svg&gt;</kbd> (200x150)<br>2. Елементом <kbd>&lt;rect&gt;</kbd> на позиції (20,20) розміром 80x60<br>3. Елементом <kbd>&lt;line&gt;</kbd> від (120,30) до (180,90) з кольором stroke",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 200px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 200px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<svg width=\"200\" height=\"150\">\n <rect x=\"20\" y=\"20\" width=\"80\" height=\"60\" fill=\"#e74c3c\" />\n <line x1=\"120\" y1=\"30\" x2=\"180\" y2=\"90\" stroke=\"#2c3e50\" stroke-width=\"3\" />\n</svg>", "solution": "<svg width=\"200\" height=\"150\">\n <rect x=\"20\" y=\"20\" width=\"80\" height=\"60\" fill=\"tomato\" />\n <line x1=\"120\" y1=\"30\" x2=\"180\" y2=\"90\" stroke=\"slategray\" stroke-width=\"3\" />\n</svg>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "svg", "value": "svg",
"message": "Add an <kbd>&lt;svg&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;svg&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "rect", "value": "rect",
"message": "Add a <kbd>&lt;rect&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;rect&gt;</kbd>"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "line", "value": "line",
"message": "Add a <kbd>&lt;line&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;line&gt;</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "width", "value": "200" },
"message": "Встановіть <kbd>width=</kbd>\"200\" в SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "height", "value": "150" },
"message": "Встановіть <kbd>height=</kbd>\"150\" в SVG"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "x", "value": "20" },
"message": "Встановіть <kbd>x=</kbd>\"20\" в rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "y", "value": "20" },
"message": "Встановіть <kbd>y=</kbd>\"20\" в rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "width", "value": "80" },
"message": "Встановіть <kbd>width=</kbd>\"80\" в rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "height", "value": "60" },
"message": "Встановіть <kbd>height=</kbd>\"60\" в rect"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "x1", "value": "120" },
"message": "Встановіть <kbd>x1=</kbd>\"120\" в line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "y1", "value": "30" },
"message": "Встановіть <kbd>y1=</kbd>\"30\" в line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "x2", "value": "180" },
"message": "Встановіть <kbd>x2=</kbd>\"180\" в line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "y2", "value": "90" },
"message": "Встановіть <kbd>y2=</kbd>\"90\" в line"
},
{
"type": "contains",
"value": "stroke",
"message": "Додайте колір <kbd>stroke</kbd> до line"
} }
] ]
}, },
{ {
"id": "svg-shapes", "id": "svg-shapes",
"title": "Multiple Shapes", "title": "Кілька фігур",
"description": "Combine shapes to create simple graphics! Add <kbd>stroke</kbd> for outlines and <kbd>stroke-width</kbd> for thickness.<br><br>Use <kbd>fill=\"none\"</kbd> for hollow shapes. Shapes stack in order - later elements appear on top.", "description": "Комбінуйте фігури для створення простої графіки! Додайте <kbd>stroke</kbd> для контурів та <kbd>stroke-width</kbd> для товщини.<br><br>Використовуйте <kbd>fill=\"none\"</kbd> для порожніх фігур. Фігури накладаються в порядку - пізніші елементи з'являються зверху.",
"task": "Create a simple face:<br>1. An <kbd>&lt;svg&gt;</kbd> (200x200)<br>2. A large <kbd>&lt;circle&gt;</kbd> for the face<br>3. Two small <kbd>&lt;circle&gt;</kbd> elements for eyes<br>4. A <kbd>&lt;line&gt;</kbd> for the smile", "task": "Створіть просте обличчя:<br>1. Елемент <kbd>&lt;svg&gt;</kbd> (200x200)<br>2. Велике <kbd>&lt;circle&gt;</kbd> для обличчя<br>3. Два маленьких <kbd>&lt;circle&gt;</kbd> для очей<br>4. Елемент <kbd>&lt;line&gt;</kbd> для посмішки",
"previewHTML": "", "previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); min-height: 250px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }", "previewBaseCSS": "body { font-family: system-ui; padding: 20px; background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); min-height: 250px; display: flex; justify-content: center; align-items: center; } svg { background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }",
"sandboxCSS": "", "sandboxCSS": "",
"initialCode": "", "initialCode": "",
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"80\" fill=\"#f1c40f\" stroke=\"#e67e22\" stroke-width=\"4\" />\n <circle cx=\"70\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <circle cx=\"130\" cy=\"80\" r=\"10\" fill=\"#2c3e50\" />\n <line x1=\"60\" y1=\"130\" x2=\"140\" y2=\"130\" stroke=\"#2c3e50\" stroke-width=\"4\" stroke-linecap=\"round\" />\n</svg>", "solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"80\" fill=\"gold\" stroke=\"orange\" stroke-width=\"4\" />\n <circle cx=\"70\" cy=\"80\" r=\"10\" fill=\"darkslategray\" />\n <circle cx=\"130\" cy=\"80\" r=\"10\" fill=\"darkslategray\" />\n <line x1=\"60\" y1=\"130\" x2=\"140\" y2=\"130\" stroke=\"darkslategray\" stroke-width=\"4\" stroke-linecap=\"round\" />\n</svg>",
"previewContainer": "preview-area", "previewContainer": "preview-area",
"validations": [ "validations": [
{ {
"type": "element_exists", "type": "element_exists",
"value": "svg", "value": "svg",
"message": "Add an <kbd>&lt;svg&gt;</kbd> element" "message": "Додайте елемент <kbd>&lt;svg&gt;</kbd>"
}, },
{ {
"type": "element_count", "type": "element_count",
"value": { "selector": "circle", "min": 3 }, "value": { "selector": "circle", "min": 3 },
"message": "Add at least 3 circles (1 face + 2 eyes)" "message": "Додайте принаймні 3 кола (1 обличчя + 2 ока)"
}, },
{ {
"type": "element_exists", "type": "element_exists",
"value": "line", "value": "line",
"message": "Add a <kbd>&lt;line&gt;</kbd> for the smile" "message": "Додайте <kbd>&lt;line&gt;</kbd> для посмішки"
} }
] ]
} }