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:
@@ -2,15 +2,15 @@
|
||||
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||
"id": "welcome",
|
||||
"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",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "get-started",
|
||||
"title": "Get Started",
|
||||
"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",
|
||||
"task": "Write <code>Hello World</code>",
|
||||
"title": "Comenzar",
|
||||
"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": "Escribe <code>Hello World</code>",
|
||||
"previewHTML": "",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; font-size: 2rem; color: #6366f1; font-weight: bold; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -21,15 +21,15 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "Hello World",
|
||||
"message": "Write <code>Hello World</code>"
|
||||
"message": "Escribe <code>Hello World</code>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "overview",
|
||||
"title": "Overview",
|
||||
"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>",
|
||||
"task": "Click Next to continue",
|
||||
"title": "Vista General",
|
||||
"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": "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>",
|
||||
"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": "",
|
||||
@@ -39,8 +39,8 @@
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "Hello World",
|
||||
"message": "Click Next to continue"
|
||||
"value": "Hello",
|
||||
"message": "Haz clic en Siguiente para continuar"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||
"id": "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",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "box-model-1",
|
||||
"title": "Box Model Components",
|
||||
"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.",
|
||||
"task": "Set <kbd>padding</kbd> to <kbd>1rem</kbd> to create space between the content and border.",
|
||||
"previewHTML": "<div class=\"box\">Box Model Components</div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: lavender; border: 2px dashed slategray; }",
|
||||
"title": "Padding",
|
||||
"description": "Cada elemento en CSS es una caja con cuatro capas: contenido, padding, borde y margen. <strong>Padding</strong> crea espacio entre tu contenido y el borde de la caja.<br><br>Sin padding, el texto se aprieta incómodamente contra los bordes. El padding hace que el contenido sea legible y visualmente equilibrado.<br><br><pre>.card {\n padding: 1rem;\n}</pre>",
|
||||
"task": "Esta tarjeta de perfil se ve apretada. Añade <kbd>padding: 1rem</kbd> para que el texto tenga espacio para respirar.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
"codePrefix": ".box {\n ",
|
||||
"codePrefix": ".card {\n ",
|
||||
"initialCode": "",
|
||||
"codeSuffix": "\n}",
|
||||
"solution": "padding: 1rem;",
|
||||
@@ -22,62 +22,62 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Set <kbd>padding: 1rem</kbd>"
|
||||
"message": "Establece <kbd>padding: 1rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "box-model-2",
|
||||
"title": "Adding Borders",
|
||||
"description": "Borders outline an element, creating visual separation from surrounding content. The border shorthand accepts three values: width, style, and color.",
|
||||
"task": "Set <kbd>border</kbd> to <kbd>2px solid darkslategray</kbd>.",
|
||||
"previewHTML": "<div class=\"box\">This box needs a border</div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background-color: mintcream; padding: 1rem; }",
|
||||
"title": "Borders",
|
||||
"description": "Los bordes crean límites visuales alrededor de los elementos. El atajo <kbd>border</kbd> acepta tres valores: ancho, estilo y color.<br><br>Estilos comunes: <kbd>solid</kbd>, <kbd>dashed</kbd>, <kbd>dotted</kbd>, <kbd>none</kbd>",
|
||||
"task": "Añade un acento sutil a la izquierda de la tarjeta con <kbd>border-left: 4px solid steelblue</kbd>.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
"codePrefix": ".box {\n ",
|
||||
"codePrefix": ".card {\n ",
|
||||
"initialCode": "",
|
||||
"codeSuffix": "\n}",
|
||||
"solution": "border: 2px solid darkslategray;",
|
||||
"solution": "border-left: 4px solid steelblue;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border:\\s*2px\\s+solid\\s+darkslategray",
|
||||
"message": "Set <kbd>border: 2px solid darkslategray</kbd>",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+steelblue",
|
||||
"message": "Establece <kbd>border-left: 4px solid steelblue</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "box-model-3",
|
||||
"title": "Adding 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.",
|
||||
"task": "Set <kbd>margin</kbd> to <kbd>1rem</kbd> to create space between this element and its neighbors.",
|
||||
"previewHTML": "<div class=\"container\"><div class=\"outer\">This box needs margins</div><div class=\"neighbor\">Adjacent element</div></div>",
|
||||
"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; }",
|
||||
"title": "Margins",
|
||||
"description": "Los márgenes crean espacio <em>fuera</em> del elemento, separándolo de sus vecinos. Mientras que el padding empuja el contenido hacia adentro, los márgenes empujan otros elementos hacia afuera.",
|
||||
"task": "Añade espacio entre estas dos tarjetas de perfil con <kbd>margin-bottom: 1rem</kbd> en <kbd>.card</kbd>.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article><article class=\"card\"><h3>Alex Rivera</h3><p>UX Designer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
"codePrefix": ".outer {\n ",
|
||||
"codePrefix": ".card {\n ",
|
||||
"initialCode": "",
|
||||
"codeSuffix": "\n}",
|
||||
"solution": "margin: 1rem;",
|
||||
"solution": "margin-bottom: 1rem;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "margin", "expected": "1rem" },
|
||||
"message": "Set <kbd>margin: 1rem</kbd>"
|
||||
"value": { "property": "margin-bottom", "expected": "1rem" },
|
||||
"message": "Establece <kbd>margin-bottom: 1rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "box-model-4",
|
||||
"title": "Box Sizing: Border-Box",
|
||||
"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.",
|
||||
"task": "Set <kbd>box-sizing</kbd> to <kbd>border-box</kbd> so padding and border are included in the width.",
|
||||
"previewHTML": "<div class=\"sizing-demo\"><div class=\"box default\">Content-box (default)</div><div class=\"box sized\">Border-box</div></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; }",
|
||||
"title": "Box Sizing",
|
||||
"description": "Por defecto, <kbd>width</kbd> solo establece el ancho del contenido. Padding y bordes se suman al total. Esto causa problemas de diseño.<br><br><kbd>box-sizing: border-box</kbd> incluye padding y borde en el ancho, haciendo el dimensionamiento predecible. La mayoría de desarrolladores aplican esto a todos los elementos.",
|
||||
"task": "Ambas tarjetas tienen <kbd>width: 200px</kbd>. La izquierda usa el tamaño predeterminado (content-box), haciéndola más ancha de lo esperado. Corrige la tarjeta derecha con <kbd>box-sizing: border-box</kbd>.",
|
||||
"previewHTML": "<div class=\"demo\"><article class=\"card\">Content-box</article><article class=\"card fix\">Border-box</article></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .demo { display: flex; gap: 1rem; } .card { width: 200px; padding: 1rem; border: 4px solid steelblue; background: white; border-radius: 8px; }",
|
||||
"sandboxCSS": "",
|
||||
"codePrefix": ".sized {\n ",
|
||||
"codePrefix": ".fix {\n ",
|
||||
"initialCode": "",
|
||||
"codeSuffix": "\n}",
|
||||
"solution": "box-sizing: border-box;",
|
||||
@@ -86,93 +86,104 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"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",
|
||||
"title": "Margin Collapse",
|
||||
"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.",
|
||||
"task": "Set <kbd>margin-bottom</kbd> to <kbd>2rem</kbd>. Notice the space between paragraphs equals 2rem (not 3rem) due to margin collapse.",
|
||||
"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>",
|
||||
"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; }",
|
||||
"title": "Padding Shorthand",
|
||||
"description": "Padding acepta 1-4 valores:<br>• 1 valor: todos los lados<br>• 2 valores: vertical | horizontal<br>• 4 valores: arriba | derecha | abajo | izquierda",
|
||||
"task": "Este botón necesita más espacio horizontal que vertical. Establece <kbd>padding: 8px 1rem</kbd> (8px arriba/abajo, 1rem izquierda/derecha).",
|
||||
"previewHTML": "<button class=\"btn\">Follow</button>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
"codePrefix": ".first {\n ",
|
||||
"codePrefix": ".btn {\n ",
|
||||
"initialCode": "",
|
||||
"codeSuffix": "\n}",
|
||||
"solution": "margin-bottom: 2rem;",
|
||||
"solution": "padding: 8px 1rem;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "margin-bottom", "expected": "2rem" },
|
||||
"message": "Set <kbd>margin-bottom: 2rem</kbd>"
|
||||
"type": "regex",
|
||||
"value": "padding:\\s*8px\\s+1rem",
|
||||
"message": "Establece <kbd>padding: 8px 1rem</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "box-model-6",
|
||||
"title": "Margin Shorthand Notation",
|
||||
"description": "The margin shorthand can set all four sides at once. Two values set vertical (top/bottom) and horizontal (left/right) margins respectively.",
|
||||
"task": "Set <kbd>margin</kbd> to <kbd>1rem 2rem</kbd> for 1rem top/bottom and 2rem left/right.",
|
||||
"previewHTML": "<div class=\"container\"><div class=\"spaced\">This box needs margins: 1rem top/bottom, 2rem left/right</div></div>",
|
||||
"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; }",
|
||||
"title": "Margin Shorthand",
|
||||
"description": "Margin usa el mismo patrón de atajo que padding. Un patrón común es centrar elementos de bloque horizontalmente con <kbd>margin: 0 auto</kbd>.",
|
||||
"task": "Centra esta tarjeta horizontalmente. Establece <kbd>margin: 0 auto</kbd> para calcular automáticamente márgenes iguales izquierda/derecha.",
|
||||
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { width: 250px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
"codePrefix": ".spaced {\n ",
|
||||
"codePrefix": ".card {\n ",
|
||||
"initialCode": "",
|
||||
"codeSuffix": "\n}",
|
||||
"solution": "margin: 1rem 2rem;",
|
||||
"solution": "margin: 0 auto;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "margin:\\s*1rem\\s+2rem",
|
||||
"message": "Set <kbd>margin: 1rem 2rem</kbd>",
|
||||
"value": "margin:\\s*0\\s+auto",
|
||||
"message": "Establece <kbd>margin: 0 auto</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "box-model-7",
|
||||
"title": "Padding Shorthand Notation",
|
||||
"description": "Like margin, padding shorthand allows setting all sides at once. A single value applies to all four sides equally.",
|
||||
"task": "Set <kbd>padding</kbd> to <kbd>2rem</kbd> to add equal padding on all sides.",
|
||||
"previewHTML": "<div class=\"padded\">This box needs equal padding on all sides</div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .padded { background-color: papayawhip; border: 2px solid orange; }",
|
||||
"title": "Border Radius",
|
||||
"description": "Aunque no es parte del modelo de caja clásico, <kbd>border-radius</kbd> redondea las esquinas de la caja de borde de un elemento. Usa <kbd>50%</kbd> en un elemento cuadrado para crear un círculo.",
|
||||
"task": "Haz la imagen del avatar circular con <kbd>border-radius: 50%</kbd>.",
|
||||
"previewHTML": "<article class=\"card\"><img class=\"avatar\" src=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='35' r='25' fill='%23666'/%3E%3Ccircle cx='50' cy='90' r='40' fill='%23666'/%3E%3C/svg%3E\" alt=\"Avatar\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; text-align: center; } .avatar { width: 80px; height: 80px; background: #ddd; margin-bottom: 8px; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
"codePrefix": ".padded {\n ",
|
||||
"codePrefix": ".avatar {\n ",
|
||||
"initialCode": "",
|
||||
"codeSuffix": "\n}",
|
||||
"solution": "padding: 2rem;",
|
||||
"solution": "border-radius: 50%;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "2rem" },
|
||||
"message": "Set <kbd>padding: 2rem</kbd>"
|
||||
"value": { "property": "border-radius", "expected": "50%" },
|
||||
"message": "Establece <kbd>border-radius: 50%</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "box-model-8",
|
||||
"title": "Border on Specific Sides",
|
||||
"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>.",
|
||||
"task": "Set <kbd>border-bottom</kbd> to <kbd>4px solid dodgerblue</kbd>.",
|
||||
"previewHTML": "<div class=\"line\">This element needs only a bottom border</div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .line { padding: 1rem; background-color: aliceblue; }",
|
||||
"title": "Complete Card",
|
||||
"description": "Combinemos todo. Esta tarjeta de notificación necesita estilo para verse profesional.",
|
||||
"task": "Estiliza la notificación: añade <kbd>padding: 1rem</kbd>, <kbd>border-left: 4px solid coral</kbd> y <kbd>border-radius: 4px</kbd>.",
|
||||
"previewHTML": "<div class=\"alert\"><strong>New message</strong><p>You have 3 unread notifications</p></div>",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { background: seashell; } .alert strong { color: coral; } .alert p { margin: 4px 0 0; color: #666; }",
|
||||
"sandboxCSS": "",
|
||||
"codePrefix": ".line {\n ",
|
||||
"codePrefix": ".alert {\n ",
|
||||
"initialCode": "",
|
||||
"codeSuffix": "\n}",
|
||||
"solution": "border-bottom: 4px solid dodgerblue;",
|
||||
"solution": "padding: 1rem;\n border-left: 4px solid coral;\n border-radius: 4px;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "padding", "expected": "1rem" },
|
||||
"message": "Establece <kbd>padding: 1rem</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "border-bottom:\\s*4px\\s+solid\\s+dodgerblue",
|
||||
"message": "Set <kbd>border-bottom: 4px solid dodgerblue</kbd>",
|
||||
"value": "border-left:\\s*4px\\s+solid\\s+coral",
|
||||
"message": "Establece <kbd>border-left: 4px solid coral</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border-radius", "expected": "4px" },
|
||||
"message": "Establece <kbd>border-radius: 4px</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,115 +1,100 @@
|
||||
{
|
||||
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||
"id": "units-variables",
|
||||
"title": "CSS Units & Variables",
|
||||
"description": "Understand the variety of CSS measurement units and how to define and use custom properties for maintainable styles.",
|
||||
"title": "Unidades y Variables CSS",
|
||||
"description": "Comprende la variedad de unidades de medida CSS y cómo definir y usar propiedades personalizadas para estilos mantenibles.",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "units-1",
|
||||
"title": "Absolute vs. Relative Units",
|
||||
"description": "Learn the difference between px, rem, em, %, and vw/vh for flexible, responsive layouts.",
|
||||
"task": "Set the <kbd>width</kbd> of <kbd>.box</kbd> to <kbd>80%</kbd> and <kbd>max-width</kbd> to <kbd>37.5rem</kbd>.",
|
||||
"previewHTML": "<div class=\"box\">Resize me!</div>",
|
||||
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { background: #f5f5f5; padding: 1rem; }",
|
||||
"title": "Relative Units",
|
||||
"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": "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": "<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: 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": "",
|
||||
"codePrefix": "/* Set flexible sizing */\n.box {",
|
||||
"codePrefix": ".article {\n ",
|
||||
"initialCode": "",
|
||||
"codeSuffix": "}",
|
||||
"solution": " width: 80%;\n max-width: 37.5rem;",
|
||||
"codeSuffix": "\n}",
|
||||
"solution": "max-width: 40rem;",
|
||||
"previewContainer": "preview-area",
|
||||
"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",
|
||||
"value": { "property": "max-width", "expected": "37.5rem" },
|
||||
"message": "Set max-width to <kbd>37.5rem</kbd>"
|
||||
"value": { "property": "max-width", "expected": "40rem" },
|
||||
"message": "Establece <kbd>max-width: 40rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "units-2",
|
||||
"title": "CSS Custom Properties",
|
||||
"description": "Define and reuse variables (--custom properties) to centralize your theme values.",
|
||||
"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>.",
|
||||
"previewHTML": "<div class=\"themed\">Variable Box</div>",
|
||||
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .themed { padding: 1rem; border: 0.125rem solid #ddd; }",
|
||||
"title": "CSS Variables",
|
||||
"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": "Define <kbd>--brand: steelblue</kbd> en <kbd>:root</kbd>, luego úsala como color de <kbd>background</kbd> para <kbd>.btn</kbd>.",
|
||||
"previewHTML": "<div class=\"actions\"><button class=\"btn\">Subscribe</button><button class=\"btn\">Learn More</button></div>",
|
||||
"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": "",
|
||||
"codePrefix": "/* Define and use a CSS variable */\n:root {",
|
||||
"codePrefix": ":root {\n ",
|
||||
"initialCode": "",
|
||||
"codeSuffix": "}\n.themed { }",
|
||||
"solution": " --main-color: #6200ee;\n}\n.themed {\n border-color: var(--main-color);",
|
||||
"codeSuffix": "\n}\n\n.btn {\n background: var(--brand);\n}",
|
||||
"solution": "--brand: steelblue;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "--main-color",
|
||||
"message": "Define <kbd>--main-color</kbd> in :root",
|
||||
"value": "--brand",
|
||||
"message": "Define la variable <kbd>--brand</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "var(--main-color)",
|
||||
"message": "Use <kbd>var(--main-color)</kbd>",
|
||||
"value": "steelblue",
|
||||
"message": "Establece el valor a <kbd>steelblue</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "border", "expected": "var(--main-color)" },
|
||||
"message": "Apply variable to border color",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "units-3",
|
||||
"title": "Unit Calculations (calc)",
|
||||
"description": "Use the <kbd>calc()</kbd> function to combine different units in one expression.",
|
||||
"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>.",
|
||||
"previewHTML": "<div class=\"sized\">Calc Demo</div>",
|
||||
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sized { background: #e8f5e9; padding: 1rem; }",
|
||||
"title": "calc() Function",
|
||||
"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": "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=\"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: 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": "",
|
||||
"codePrefix": "/* Use calc for dynamic sizing */\n.sized {",
|
||||
"codePrefix": ".main {\n ",
|
||||
"initialCode": "",
|
||||
"codeSuffix": "}",
|
||||
"solution": " width: calc(100% - 2rem);\n min-height: calc(10vh + 1rem);",
|
||||
"codeSuffix": "\n}",
|
||||
"solution": "width: calc(100% - 200px);",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{ "type": "contains", "value": "calc", "message": "Use <kbd>calc()</kbd> function", "options": { "caseSensitive": false } },
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "width:\\s*calc\\(100% - 2rem\\)",
|
||||
"message": "Width should be <kbd>calc(100% - 2rem)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "min-height:\\s*calc\\(10vh \\+ 1rem\\)",
|
||||
"message": "Min-height should be <kbd>calc(10vh + 1rem)</kbd>",
|
||||
"value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
|
||||
"message": "Establece <kbd>width: calc(100% - 200px)</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "units-4",
|
||||
"title": "Viewport & Responsive Units",
|
||||
"description": "Control layouts relative to viewport size with vw, vh, and vmin/vmax units.",
|
||||
"task": "Give <kbd>.view</kbd> a <kbd>width</kbd> of <kbd>50vw</kbd> and <kbd>height</kbd> of <kbd>20vh</kbd>.",
|
||||
"previewHTML": "<div class=\"view\">Viewport Box</div>",
|
||||
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .view { background: #ffe0b2; }",
|
||||
"title": "Viewport 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": "Haz que esta sección hero llene la altura del viewport estableciendo <kbd>min-height: 100vh</kbd>.",
|
||||
"previewHTML": "<section class=\"hero\"><h1>Welcome</h1><p>Scroll down to explore</p></section>",
|
||||
"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": "",
|
||||
"codePrefix": "/* Use viewport units */\n.view {",
|
||||
"codePrefix": ".hero {\n ",
|
||||
"initialCode": "",
|
||||
"codeSuffix": "}",
|
||||
"solution": " width: 50vw;\n height: 20vh;",
|
||||
"codeSuffix": "\n}",
|
||||
"solution": "min-height: 100vh;",
|
||||
"previewContainer": "preview-area",
|
||||
"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", "value": { "property": "width", "expected": "50vw" }, "message": "Set width to <kbd>50vw</kbd>" },
|
||||
{ "type": "property_value", "value": { "property": "height", "expected": "20vh" }, "message": "Set height to <kbd>20vh</kbd>" }
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "min-height", "expected": "100vh" },
|
||||
"message": "Establece <kbd>min-height: 100vh</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||
"id": "transitions-animations",
|
||||
"title": "CSS Animations",
|
||||
"description": "Bring interactivity to your UI by smoothly transitioning properties and creating keyframe-driven animations.",
|
||||
"title": "Animaciones CSS",
|
||||
"description": "Añade interactividad a tu UI mediante transiciones suaves de propiedades y animaciones basadas en keyframes.",
|
||||
"difficulty": "intermediate",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "transitions-1",
|
||||
"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>",
|
||||
"task": "Add <kbd>transition: background-color 0.3s</kbd> to <kbd>.btn</kbd> so the color fades smoothly on hover.",
|
||||
"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": "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>",
|
||||
"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": "",
|
||||
@@ -22,13 +22,13 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "transition",
|
||||
"message": "Use the <kbd>transition</kbd> property",
|
||||
"message": "Usa la propiedad <kbd>transition</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"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 }
|
||||
}
|
||||
]
|
||||
@@ -36,8 +36,8 @@
|
||||
{
|
||||
"id": "transitions-2",
|
||||
"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.",
|
||||
"task": "Set <kbd>transition-timing-function</kbd> to <kbd>ease-in-out</kbd> on <kbd>.btn</kbd>.",
|
||||
"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": "Establece <kbd>transition-timing-function</kbd> a <kbd>ease-in-out</kbd>.",
|
||||
"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); }",
|
||||
"sandboxCSS": "",
|
||||
@@ -50,21 +50,21 @@
|
||||
{
|
||||
"type": "contains",
|
||||
"value": "transition-timing-function",
|
||||
"message": "Use <kbd>transition-timing-function</kbd>",
|
||||
"message": "Usa <kbd>transition-timing-function</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"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",
|
||||
"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>",
|
||||
"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>.",
|
||||
"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 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>",
|
||||
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .ball { width: 50px; height: 50px; background: crimson; border-radius: 50%; margin: 2rem auto; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -83,19 +83,19 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"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 }
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
"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 }
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"value": "animation:.*bounce.*1s.*infinite",
|
||||
"message": "Apply <kbd>animation: bounce 1s infinite</kbd>",
|
||||
"message": "Aplica <kbd>animation: bounce 1s infinite</kbd>",
|
||||
"options": { "caseSensitive": false }
|
||||
}
|
||||
]
|
||||
@@ -103,8 +103,8 @@
|
||||
{
|
||||
"id": "transitions-4",
|
||||
"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>.",
|
||||
"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>.",
|
||||
"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": "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>",
|
||||
"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": "",
|
||||
@@ -117,27 +117,27 @@
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-name", "expected": "pulse" },
|
||||
"message": "Set <kbd>animation-name: pulse</kbd>"
|
||||
"message": "Establece <kbd>animation-name: pulse</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-duration", "expected": "2s" },
|
||||
"message": "Set <kbd>animation-duration: 2s</kbd>"
|
||||
"message": "Establece <kbd>animation-duration: 2s</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "animation-delay", "expected": "1s" },
|
||||
"message": "Set <kbd>animation-delay: 1s</kbd>"
|
||||
"message": "Establece <kbd>animation-delay: 1s</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"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",
|
||||
"value": { "property": "animation-fill-mode", "expected": "forwards" },
|
||||
"message": "Set <kbd>animation-fill-mode: forwards</kbd>"
|
||||
"message": "Establece <kbd>animation-fill-mode: forwards</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||
"id": "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",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "responsive-1",
|
||||
"title": "Media Queries",
|
||||
"description": "Understand the syntax and use cases for CSS media queries to apply styles conditionally based on viewport characteristics.",
|
||||
"task": "Write a media query with <kbd>@media (max-width: 600px)</kbd> that changes <kbd>.panel</kbd> background to <kbd>lightcoral</kbd>.",
|
||||
"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": "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>",
|
||||
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .panel { padding: 1rem; background: lightblue; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -22,19 +22,19 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"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 }
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
"value": ".panel",
|
||||
"message": "Target <kbd>.panel</kbd> inside the media query",
|
||||
"message": "Selecciona <kbd>.panel</kbd> dentro de la media query",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "background", "expected": "lightcoral" },
|
||||
"message": "Set <kbd>background: lightcoral</kbd>",
|
||||
"message": "Establece <kbd>background: lightcoral</kbd>",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
@@ -42,8 +42,8 @@
|
||||
{
|
||||
"id": "responsive-2",
|
||||
"title": "Fluid Type",
|
||||
"description": "Use relative units like <kbd>vw</kbd> to make font sizes scale with the viewport width.",
|
||||
"task": "Set <kbd>font-size: 5vw</kbd> on <kbd>.text</kbd> so it scales as the viewport changes.",
|
||||
"description": "Usa unidades relativas como <kbd>vw</kbd> para que los tamaños de fuente escalen con el ancho del viewport.",
|
||||
"task": "Establece <kbd>font-size: 5vw</kbd> para que escale con el viewport.",
|
||||
"previewHTML": "<p class=\"text\">Fluid Typography</p>",
|
||||
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -53,46 +53,50 @@
|
||||
"solution": " font-size: 5vw;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Set <kbd>font-size: 5vw</kbd>" }
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "font-size", "expected": "5vw" },
|
||||
"message": "Establece <kbd>font-size: 5vw</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "responsive-3",
|
||||
"title": "Flex Grids",
|
||||
"description": "Combine CSS Grid with <kbd>auto-fit</kbd> or <kbd>auto-fill</kbd> for responsive column layouts.",
|
||||
"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>.",
|
||||
"previewHTML": "<div class=\"cards\"><div>1</div><div>2</div><div>3</div><div>4</div></div>",
|
||||
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .cards > div { background: #d1c4e9; padding: 1rem; }",
|
||||
"title": "Responsive Grid",
|
||||
"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": "Añade <kbd>display: grid</kbd>, <kbd>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</kbd> y <kbd>gap: 1rem</kbd>.",
|
||||
"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: 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": "",
|
||||
"codePrefix": "/* Create a responsive grid */\n.cards {",
|
||||
"codePrefix": ".features {\n ",
|
||||
"initialCode": "",
|
||||
"codeSuffix": "}",
|
||||
"solution": " display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;",
|
||||
"codeSuffix": "\n}",
|
||||
"solution": "display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "display", "expected": "grid" },
|
||||
"message": "Set <kbd>display: grid</kbd>"
|
||||
"message": "Establece <kbd>display: grid</kbd>"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"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 }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "gap", "expected": "1rem" },
|
||||
"message": "Set <kbd>gap: 1rem</kbd>"
|
||||
"message": "Establece <kbd>gap: 1rem</kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "responsive-4",
|
||||
"title": "Mobile-First",
|
||||
"description": "Adopt a mobile-first approach by writing base styles for small screens and enhancing for larger viewports.",
|
||||
"task": "Write a media query with <kbd>@media (min-width: 768px)</kbd> that sets <kbd>.sidebar</kbd> width to <kbd>250px</kbd>.",
|
||||
"description": "Adopta un enfoque mobile-first escribiendo estilos base para pantallas pequeñas y mejorándolos para viewports más grandes.",
|
||||
"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>",
|
||||
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -105,19 +109,19 @@
|
||||
{
|
||||
"type": "regex",
|
||||
"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 }
|
||||
},
|
||||
{
|
||||
"type": "contains",
|
||||
"value": ".sidebar",
|
||||
"message": "Target <kbd>.sidebar</kbd> inside media query",
|
||||
"message": "Selecciona <kbd>.sidebar</kbd> en la media query",
|
||||
"options": { "caseSensitive": false }
|
||||
},
|
||||
{
|
||||
"type": "property_value",
|
||||
"value": { "property": "width", "expected": "250px" },
|
||||
"message": "Set <kbd>width: 250px</kbd>",
|
||||
"message": "Establece <kbd>width: 250px</kbd>",
|
||||
"options": { "exact": false }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,65 +2,65 @@
|
||||
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||
"id": "html-elements",
|
||||
"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",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "block-vs-inline-intro",
|
||||
"title": "Block vs Inline Elements",
|
||||
"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><div></kbd>, <kbd><p></kbd>, <kbd><h1></kbd>, <kbd><section></kbd><br><br><strong>Inline elements</strong> flow within text and only take needed width. Examples: <kbd><span></kbd>, <kbd><a></kbd>, <kbd><strong></kbd>, <kbd><em></kbd>",
|
||||
"task": "Wrap the word <kbd>important</kbd> with <kbd><strong></kbd> tags to make it bold. Notice how the paragraph (block) takes full width while strong (inline) flows with text.",
|
||||
"title": "Elementos de bloque vs en línea",
|
||||
"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><div></kbd>, <kbd><p></kbd>, <kbd><h1></kbd>, <kbd><section></kbd><br><br><strong>Elementos en línea</strong> fluyen dentro del texto y solo ocupan el ancho necesario. Ejemplos: <kbd><span></kbd>, <kbd><a></kbd>, <kbd><strong></kbd>, <kbd><em></kbd>",
|
||||
"task": "Envuelve la palabra <kbd>importante</kbd> con etiquetas <kbd><strong></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": "",
|
||||
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; } p { background: #e3f2fd; padding: 10px; } strong { background: #ffecb3; }",
|
||||
"sandboxCSS": "",
|
||||
"initialCode": "<p>This is a paragraph with an important word.</p>",
|
||||
"solution": "<p>This is a paragraph with an <strong>important</strong> word.</p>",
|
||||
"initialCode": "<p>Este es un párrafo con una palabra importante.</p>",
|
||||
"solution": "<p>Este es un párrafo con una palabra <strong>importante</strong>.</p>",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "p",
|
||||
"message": "Add a <kbd><p></kbd> paragraph element"
|
||||
"message": "Añade un elemento de párrafo <kbd><p></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "parent_child",
|
||||
"value": { "parent": "p", "child": "strong" },
|
||||
"message": "Wrap the word <kbd>important</kbd> with <kbd><strong></kbd> tags"
|
||||
"message": "Envuelve la palabra <kbd>importante</kbd> con etiquetas <kbd><strong></kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "semantic-containers",
|
||||
"title": "Semantic Tags",
|
||||
"description": "Modern HTML uses semantic containers that describe their content:<br><br><kbd><header></kbd> - Page or section header<br><kbd><nav></kbd> - Navigation links<br><kbd><main></kbd> - Main content area<br><kbd><section></kbd> - Thematic grouping<br><kbd><article></kbd> - Self-contained content<br><kbd><footer></kbd> - Page or section footer",
|
||||
"task": "Create a basic page structure:<br>1. Add a <kbd><header></kbd> with an <kbd><h1></kbd> containing the text <code>My Website</code><br>2. Add a <kbd><main></kbd> element with a paragraph saying <code>Welcome to my site!</code><br>3. Add a <kbd><footer></kbd> with a paragraph saying <code>Copyright 2026</code>",
|
||||
"title": "Etiquetas semánticas",
|
||||
"description": "El HTML moderno usa contenedores semánticos que describen su contenido:<br><br><kbd><header></kbd> - Encabezado de página o sección<br><kbd><nav></kbd> - Enlaces de navegación<br><kbd><main></kbd> - Área de contenido principal<br><kbd><section></kbd> - Agrupación temática<br><kbd><article></kbd> - Contenido independiente<br><kbd><footer></kbd> - Pie de página o sección",
|
||||
"task": "Crea una estructura básica de página:<br>1. Añade un <kbd><header></kbd> con un <kbd><h1></kbd> que contenga el texto <code>Mi Sitio Web</code><br>2. Añade un elemento <kbd><main></kbd> con un párrafo que diga <code>¡Bienvenido a mi sitio!</code><br>3. Añade un <kbd><footer></kbd> con un párrafo que diga <code>Copyright 2026</code>",
|
||||
"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; }",
|
||||
"sandboxCSS": "",
|
||||
"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",
|
||||
"validations": [
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "header",
|
||||
"message": "Add a <kbd><header></kbd> element"
|
||||
"message": "Añade un elemento <kbd><header></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "main",
|
||||
"message": "Add a <kbd><main></kbd> element"
|
||||
"message": "Añade un elemento <kbd><main></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "footer",
|
||||
"message": "Add a <kbd><footer></kbd> element"
|
||||
"message": "Añade un elemento <kbd><footer></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "parent_child",
|
||||
"value": { "parent": "header", "child": "h1" },
|
||||
"message": "Add an <kbd><h1></kbd> heading inside your header"
|
||||
"message": "Añade un encabezado <kbd><h1></kbd> dentro de tu header"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,100 +1,100 @@
|
||||
{
|
||||
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||
"id": "html-forms-basic",
|
||||
"title": "HTML Forms",
|
||||
"description": "Learn to create forms with various input types",
|
||||
"title": "Formularios HTML",
|
||||
"description": "Aprende a crear formularios con varios tipos de campos",
|
||||
"mode": "html",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "form-structure",
|
||||
"title": "Form Structure",
|
||||
"description": "Every form needs a <kbd><form></kbd> wrapper. Inside, use <kbd><label></kbd> to describe inputs and <kbd><input></kbd> for user data entry.<br><br>The <kbd>for</kbd> attribute on labels should match the <kbd>id</kbd> on inputs for accessibility.",
|
||||
"task": "Create a form with:<br>1. A <kbd><label></kbd> with the text <code>Name:</code> and <kbd>for=\"name\"</kbd> attribute<br>2. A text <kbd><input></kbd> with <kbd>id=\"name\"</kbd> and <kbd>name=\"name\"</kbd> attributes",
|
||||
"title": "Estructura del formulario",
|
||||
"description": "Todo formulario necesita un contenedor <kbd><form></kbd>. Dentro, usa <kbd><label></kbd> para describir campos y <kbd><input></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": "Crea un formulario con:<br>1. Un <kbd><label></kbd> con el texto <code>Nombre:</code> y el atributo <kbd>for=\"name\"</kbd><br>2. Un <kbd><input></kbd> de texto con los atributos <kbd>id=\"name\"</kbd> y <kbd>name=\"name\"</kbd>",
|
||||
"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; }",
|
||||
"sandboxCSS": "",
|
||||
"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",
|
||||
"validations": [
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "form",
|
||||
"message": "Wrap everything in a <kbd><form></kbd> element"
|
||||
"message": "Envuelve todo en un elemento <kbd><form></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "label",
|
||||
"message": "Add a <kbd><label></kbd> for your input"
|
||||
"message": "Añade un <kbd><label></kbd> para tu campo"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "input",
|
||||
"message": "Add an <kbd><input></kbd> element"
|
||||
"message": "Añade un elemento <kbd><input></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "attribute_value",
|
||||
"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",
|
||||
"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",
|
||||
"title": "Input Types",
|
||||
"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",
|
||||
"task": "Create a login form with two fields:<br>1. An email field: <kbd><label for=\"email\">Email:</label></kbd> and <kbd><input type=\"email\" id=\"email\"></kbd><br>2. A password field: <kbd><label for=\"password\">Password:</label></kbd> and <kbd><input type=\"password\" id=\"password\"></kbd>",
|
||||
"title": "Tipos de campos",
|
||||
"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": "Crea un formulario de inicio de sesión con dos campos:<br>1. Campo de email: <kbd><label for=\"email\">Email:</label></kbd> y <kbd><input type=\"email\" id=\"email\"></kbd><br>2. Campo de contraseña: <kbd><label for=\"password\">Contraseña:</label></kbd> y <kbd><input type=\"password\" id=\"password\"></kbd>",
|
||||
"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; }",
|
||||
"sandboxCSS": "",
|
||||
"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",
|
||||
"validations": [
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "input[type='email']",
|
||||
"message": "Add an input with type=\"email\""
|
||||
"message": "Añade un campo con type=\"email\""
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "input[type='password']",
|
||||
"message": "Add an input with type=\"password\""
|
||||
"message": "Añade un campo con type=\"password\""
|
||||
},
|
||||
{
|
||||
"type": "element_count",
|
||||
"value": { "selector": "label", "min": 2 },
|
||||
"message": "Add labels for both inputs"
|
||||
"message": "Añade labels para ambos campos"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "submit-button",
|
||||
"title": "Submit Button",
|
||||
"description": "Forms need a way to submit data. Use:<br><br><kbd><button type=\"submit\"></kbd> - Preferred, flexible content<br><kbd><input type=\"submit\"></kbd> - Simple text-only button<br><br>The button text should be action-oriented (e.g., <code>Sign In</code>, 'Register', 'Send').",
|
||||
"task": "Add a submit button to the form with the text <code>Sign In</code>.",
|
||||
"title": "Botón de envío",
|
||||
"description": "Los formularios necesitan una forma de enviar datos. Usa:<br><br><kbd><button type=\"submit\"></kbd> - Preferido, contenido flexible<br><kbd><input type=\"submit\"></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": "Añade un botón de envío al formulario con el texto <code>Iniciar Sesión</code>.",
|
||||
"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; }",
|
||||
"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>",
|
||||
"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>",
|
||||
"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\">Contraseña:</label>\n <input type=\"password\" id=\"password\">\n \n <button type=\"submit\">Iniciar Sesión</button>\n</form>",
|
||||
"previewContainer": "preview-area",
|
||||
"validations": [
|
||||
{
|
||||
"type": "element_exists",
|
||||
"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",
|
||||
"value": { "selector": "button", "text": "Sign In" },
|
||||
"message": "The button should say <kbd>Sign In</kbd>"
|
||||
"value": { "selector": "button", "text": "Iniciar Sesión" },
|
||||
"message": "El botón debe decir <kbd>Iniciar Sesión</kbd>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,110 +1,32 @@
|
||||
{
|
||||
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||
"id": "html-forms-validation",
|
||||
"title": "HTML Validation",
|
||||
"description": "Learn HTML5 built-in form validation attributes",
|
||||
"title": "Validación de formularios",
|
||||
"description": "Usa la validación integrada de HTML5 para mejor experiencia de usuario",
|
||||
"mode": "html",
|
||||
"difficulty": "intermediate",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "required-fields",
|
||||
"title": "Required Fields",
|
||||
"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><input type=\"text\" required></kbd><br><br>The browser shows a validation message automatically.",
|
||||
"task": "Make both the name and email fields required by adding the <kbd>required</kbd> attribute.",
|
||||
"title": "Campos requeridos",
|
||||
"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><input type=\"text\" required></kbd>",
|
||||
"task": "Haz que ambos campos (nombre y email) sean requeridos añadiendo el atributo <kbd>required</kbd> a cada campo.",
|
||||
"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": "",
|
||||
"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>",
|
||||
"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>",
|
||||
"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\">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",
|
||||
"validations": [
|
||||
{
|
||||
"type": "attribute_value",
|
||||
"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",
|
||||
"value": { "selector": "input[name='email']", "attr": "required", "value": true },
|
||||
"message": "Add the <kbd>required</kbd> attribute to the email input"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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>"
|
||||
"message": "Añade <kbd>required</kbd> al campo de email"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||
"id": "html-details-summary",
|
||||
"title": "HTML Details & Summary",
|
||||
"description": "Create expandable content sections without JavaScript",
|
||||
"description": "Crea secciones expandibles sin JavaScript",
|
||||
"mode": "html",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "details-summary-basic",
|
||||
"title": "First Widget",
|
||||
"description": "The <kbd><details></kbd> element creates a collapsible section. The <kbd><summary></kbd> provides the clickable label.<br><br>Click the summary to toggle the hidden content - no JavaScript needed!",
|
||||
"task": "Create a <kbd><details></kbd> element with:<br>1. A <kbd><summary></kbd> saying <code>Click to reveal</code><br>2. A <kbd><p></kbd> with the text <code>This content was hidden!</code>",
|
||||
"title": "Primer widget",
|
||||
"description": "El elemento <kbd><details></kbd> crea una sección plegable. El <kbd><summary></kbd> proporciona la etiqueta clickeable.<br><br>¡Haz clic en el resumen para mostrar el contenido oculto - sin JavaScript!",
|
||||
"task": "Crea un elemento <kbd><details></kbd> con:<br>1. Un <kbd><summary></kbd> que diga <code>Click to reveal</code><br>2. Un <kbd><p></kbd> con el texto <code>This content was hidden!</code>",
|
||||
"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; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -21,30 +21,30 @@
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "details",
|
||||
"message": "Add a <kbd><details></kbd> element"
|
||||
"message": "Añade un elemento <kbd><details></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "summary",
|
||||
"message": "Add a <kbd><summary></kbd> inside the details"
|
||||
"message": "Añade un <kbd><summary></kbd> dentro del details"
|
||||
},
|
||||
{
|
||||
"type": "parent_child",
|
||||
"value": { "parent": "details", "child": "summary" },
|
||||
"message": "The <kbd><summary></kbd> must be inside <kbd><details></kbd>"
|
||||
"message": "El <kbd><summary></kbd> debe estar dentro de <kbd><details></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "parent_child",
|
||||
"value": { "parent": "details", "child": "p" },
|
||||
"message": "Add a <kbd><p></kbd> inside <kbd><details></kbd> for the hidden content"
|
||||
"message": "Añade un <kbd><p></kbd> dentro de <kbd><details></kbd> para el contenido oculto"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "details-open-attribute",
|
||||
"title": "Pre-expanded Details",
|
||||
"description": "By default, <kbd><details></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.",
|
||||
"task": "Add the <kbd>open</kbd> attribute to the <kbd><details></kbd> element to show the content by default.",
|
||||
"title": "Expandido por defecto",
|
||||
"description": "Por defecto, <kbd><details></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": "Añade el atributo <kbd>open</kbd> al elemento <kbd><details></kbd> para mostrar el contenido por defecto.",
|
||||
"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; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -55,15 +55,15 @@
|
||||
{
|
||||
"type": "attribute_value",
|
||||
"value": { "selector": "details", "attr": "open", "value": true },
|
||||
"message": "Add the <kbd>open</kbd> attribute to <kbd><details></kbd>"
|
||||
"message": "Añade el atributo <kbd>open</kbd> a <kbd><details></kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "faq-accordion",
|
||||
"title": "FAQ Accordion",
|
||||
"description": "Multiple <kbd><details></kbd> elements create an accordion-style FAQ. Each question can be expanded independently.<br><br><b>Pro tip:</b> Type <kbd>details*3>summary+p</kbd> and press Tab for Emmet expansion. <kbd>*3</kbd> creates 3 elements, <kbd>></kbd> nests inside, <kbd>+</kbd> adds siblings.",
|
||||
"task": "Create an FAQ section with:<br>1. An <kbd><h1></kbd> saying <code>Frequently Asked Questions</code><br>2. Three <kbd><details></kbd> elements, each with a question in <kbd><summary></kbd> and an answer in <kbd><p></kbd>",
|
||||
"title": "Acordeón FAQ",
|
||||
"description": "Múltiples elementos <kbd><details></kbd> crean un FAQ estilo acordeón. Cada pregunta puede expandirse independientemente.<br><br><b>Pro tip:</b> Escribe <kbd>details*3>summary+p</kbd> y presiona Tab para expansión Emmet. <kbd>*3</kbd> crea 3 elementos, <kbd>></kbd> anida dentro, <kbd>+</kbd> añade hermanos.",
|
||||
"task": "Crea una sección FAQ con:<br>1. Un <kbd><h1></kbd> que diga <code>Frequently Asked Questions</code><br>2. Tres elementos <kbd><details></kbd>, cada uno con una pregunta en <kbd><summary></kbd> y una respuesta en <kbd><p></kbd>",
|
||||
"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; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -74,22 +74,22 @@
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "h1",
|
||||
"message": "Add an <kbd><h1></kbd> heading for the FAQ title"
|
||||
"message": "Añade un encabezado <kbd><h1></kbd> para el título del FAQ"
|
||||
},
|
||||
{
|
||||
"type": "element_count",
|
||||
"value": { "selector": "details", "min": 3 },
|
||||
"message": "Create at least 3 <kbd><details></kbd> elements for the FAQ"
|
||||
"message": "Crea al menos 3 elementos <kbd><details></kbd> para el FAQ"
|
||||
},
|
||||
{
|
||||
"type": "element_count",
|
||||
"value": { "selector": "summary", "min": 3 },
|
||||
"message": "Each <kbd><details></kbd> needs a <kbd><summary></kbd> for the question"
|
||||
"message": "Cada <kbd><details></kbd> necesita un <kbd><summary></kbd> para la pregunta"
|
||||
},
|
||||
{
|
||||
"type": "element_count",
|
||||
"value": { "selector": "details p", "min": 3 },
|
||||
"message": "Each <kbd><details></kbd> needs a <kbd><p></kbd> for the answer"
|
||||
"message": "Cada <kbd><details></kbd> necesita un <kbd><p></kbd> para la respuesta"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||
"id": "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",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "progress-basic",
|
||||
"title": "Progress Bars",
|
||||
"description": "The <kbd><progress></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><progress>...</progress></kbd> with fallback text inside for older browsers.",
|
||||
"task": "Create a progress bar showing 70% completion:<br>1. Add a <kbd><label></kbd> saying <code>Download:</code><br>2. Add a <kbd><progress></kbd> with <kbd>value=\"70\"</kbd> and <kbd>max=\"100\"</kbd>",
|
||||
"title": "Barras de progreso",
|
||||
"description": "El elemento <kbd><progress></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><progress>...</progress></kbd> con texto alternativo dentro para navegadores antiguos.",
|
||||
"task": "Crea una barra de progreso mostrando 70% de completado:<br>1. Añade un <kbd><label></kbd> que diga <code>Download:</code><br>2. Añade un <kbd><progress></kbd> con <kbd>value=\"70\"</kbd> y <kbd>max=\"100\"</kbd>",
|
||||
"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; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -21,30 +21,30 @@
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "progress",
|
||||
"message": "Add a <kbd><progress></kbd> element"
|
||||
"message": "Añade un elemento <kbd><progress></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "attribute_value",
|
||||
"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",
|
||||
"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",
|
||||
"value": "label",
|
||||
"message": "Add a <kbd><label></kbd> for the progress bar"
|
||||
"message": "Añade un <kbd><label></kbd> para la barra de progreso"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "progress-indeterminate",
|
||||
"title": "Indeterminate Progress",
|
||||
"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.",
|
||||
"task": "Create a loading indicator:<br>1. Add a <kbd><p></kbd> saying <code>Loading...</code><br>2. Add a <kbd><progress></kbd> without a value attribute",
|
||||
"title": "Progreso indeterminado",
|
||||
"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": "Crea un indicador de carga:<br>1. Añade un <kbd><p></kbd> que diga <code>Loading...</code><br>2. Añade un <kbd><progress></kbd> sin atributo value",
|
||||
"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; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -55,20 +55,20 @@
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "progress",
|
||||
"message": "Add a <kbd><progress></kbd> element"
|
||||
"message": "Añade un elemento <kbd><progress></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "p",
|
||||
"message": "Add a <kbd><p></kbd> with loading text"
|
||||
"message": "Añade un <kbd><p></kbd> con texto de carga"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "meter-gauge",
|
||||
"title": "Meter Gauges",
|
||||
"description": "The <kbd><meter></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!",
|
||||
"task": "Create a battery level meter:<br>1. Add a <kbd><label></kbd> saying <code>Battery:</code><br>2. Add a <kbd><meter></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>",
|
||||
"title": "Indicadores meter",
|
||||
"description": "El elemento <kbd><meter></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": "Crea un indicador de nivel de batería:<br>1. Añade un <kbd><label></kbd> que diga <code>Battery:</code><br>2. Añade un <kbd><meter></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": "",
|
||||
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } label { display: block; margin-bottom: 8px; font-weight: 500; } meter { width: 100%; height: 25px; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -79,22 +79,42 @@
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "meter",
|
||||
"message": "Add a <kbd><meter></kbd> element"
|
||||
"message": "Añade un elemento <kbd><meter></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "attribute_value",
|
||||
"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",
|
||||
"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",
|
||||
"value": "label",
|
||||
"message": "Add a <kbd><label></kbd> for the meter"
|
||||
"message": "Añade un <kbd><label></kbd> para el meter"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||
"id": "html-datalist",
|
||||
"title": "Datalist",
|
||||
"description": "Provide suggestions for text inputs without JavaScript",
|
||||
"description": "Proporciona sugerencias para campos de texto sin JavaScript",
|
||||
"mode": "html",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "datalist-basic",
|
||||
"title": "Input with Suggestions",
|
||||
"description": "The <kbd><datalist></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!",
|
||||
"task": "Create a browser selector:<br>1. Add a <kbd><label></kbd> saying <code>Browser:</code><br>2. Add an <kbd><input></kbd> with <kbd>list=\"browsers\"</kbd><br>3. Add a <kbd><datalist id=\"browsers\"></kbd> with options for Chrome, Firefox, and Safari",
|
||||
"title": "Campo con sugerencias",
|
||||
"description": "El elemento <kbd><datalist></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": "Crea un selector de navegador:<br>1. Añade un <kbd><label></kbd> que diga <code>Navegador:</code><br>2. Añade un <kbd><input></kbd> con <kbd>list=\"browsers\"</kbd><br>3. Añade un <kbd><datalist id=\"browsers\"></kbd> con opciones para Chrome, Firefox y Safari",
|
||||
"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; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -21,30 +21,30 @@
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "datalist",
|
||||
"message": "Add a <kbd><datalist></kbd> element"
|
||||
"message": "Añade un elemento <kbd><datalist></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "attribute_value",
|
||||
"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",
|
||||
"value": { "selector": "option", "min": 3 },
|
||||
"message": "Add at least 3 <kbd><option></kbd> elements inside <kbd><datalist></kbd>"
|
||||
"message": "Añade al menos 3 elementos <kbd><option></kbd> dentro de <kbd><datalist></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "label",
|
||||
"message": "Add a <kbd><label></kbd> for the input"
|
||||
"message": "Añade un <kbd><label></kbd> para el campo"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "datalist-countries",
|
||||
"title": "Country Selector",
|
||||
"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.",
|
||||
"task": "Create a country input:<br>1. Add a <kbd><label></kbd> saying <code>Country:</code><br>2. Add an <kbd><input></kbd> with <kbd>list=\"countries\"</kbd><br>3. Add a <kbd><datalist id=\"countries\"></kbd> with at least 4 country options",
|
||||
"title": "Selector de países",
|
||||
"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": "Crea un campo de país:<br>1. Añade un <kbd><label></kbd> que diga <code>País:</code><br>2. Añade un <kbd><input></kbd> con <kbd>list=\"countries\"</kbd><br>3. Añade un <kbd><datalist id=\"countries\"></kbd> con al menos 4 opciones de países",
|
||||
"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); }",
|
||||
"sandboxCSS": "",
|
||||
@@ -55,22 +55,22 @@
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "datalist",
|
||||
"message": "Add a <kbd><datalist></kbd> element"
|
||||
"message": "Añade un elemento <kbd><datalist></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "attribute_value",
|
||||
"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",
|
||||
"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",
|
||||
"value": { "selector": "option", "min": 4 },
|
||||
"message": "Add at least 4 country options"
|
||||
"message": "Añade al menos 4 opciones de países"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||
"id": "html-dialog",
|
||||
"title": "Dialogs",
|
||||
"description": "Create modal dialogs without JavaScript libraries",
|
||||
"title": "Diálogos",
|
||||
"description": "Crea diálogos modales sin bibliotecas JavaScript",
|
||||
"mode": "html",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "dialog-basic",
|
||||
"title": "Open Dialog",
|
||||
"description": "The <kbd><dialog></kbd> element creates a native modal. Add the <kbd>open</kbd> attribute to show it.<br><br>Use <kbd><form method=\"dialog\"></kbd> inside to close it when the form submits - no JavaScript needed!",
|
||||
"task": "Create a dialog with:<br>1. The <kbd>open</kbd> attribute to show it<br>2. An <kbd><h2></kbd> saying <code>Welcome!</code><br>3. A <kbd><p></kbd> with a greeting message<br>4. A <kbd><form method=\"dialog\"></kbd> with a close button",
|
||||
"title": "Abrir diálogo",
|
||||
"description": "El elemento <kbd><dialog></kbd> crea un modal nativo. Añade el atributo <kbd>open</kbd> para mostrarlo.<br><br>¡Usa <kbd><form method=\"dialog\"></kbd> dentro para cerrarlo al enviar el formulario - sin JavaScript!",
|
||||
"task": "Crea un diálogo con:<br>1. El atributo <kbd>open</kbd> para mostrarlo<br>2. Un <kbd><h2></kbd> que diga <code>¡Bienvenido!</code><br>3. Un <kbd><p></kbd> con un mensaje de saludo<br>4. Un <kbd><form method=\"dialog\"></kbd> con un botón de cerrar",
|
||||
"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; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -21,35 +21,35 @@
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "dialog",
|
||||
"message": "Add a <kbd><dialog></kbd> element"
|
||||
"message": "Añade un elemento <kbd><dialog></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "attribute_value",
|
||||
"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",
|
||||
"value": "dialog h2",
|
||||
"message": "Add an <kbd><h2></kbd> heading inside the dialog"
|
||||
"message": "Añade un encabezado <kbd><h2></kbd> dentro del diálogo"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "form[method='dialog']",
|
||||
"message": "Add a <kbd><form method=\"dialog\"></kbd> for closing"
|
||||
"message": "Añade un <kbd><form method=\"dialog\"></kbd> para cerrar"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"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",
|
||||
"title": "Dialog + Form",
|
||||
"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.",
|
||||
"task": "Create a confirmation dialog:<br>1. Add <kbd>open</kbd> to show it<br>2. An <kbd><h2></kbd> saying <code>Confirm Delete</code><br>3. A <kbd><p></kbd> asking <code>Are you sure?</code><br>4. A <kbd><form method=\"dialog\"></kbd> with Cancel and Delete buttons",
|
||||
"title": "Diálogo + Formulario",
|
||||
"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": "Crea un diálogo de confirmación:<br>1. Añade <kbd>open</kbd> para mostrarlo<br>2. Un <kbd><h2></kbd> que diga <code>Confirmar eliminación</code><br>3. Un <kbd><p></kbd> preguntando <code>¿Estás seguro?</code><br>4. Un <kbd><form method=\"dialog\"></kbd> con botones Cancelar y Eliminar",
|
||||
"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; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -60,22 +60,22 @@
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "dialog[open]",
|
||||
"message": "Add a <kbd><dialog></kbd> with the open attribute"
|
||||
"message": "Añade un <kbd><dialog></kbd> con el atributo open"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "dialog h2",
|
||||
"message": "Add a heading to the dialog"
|
||||
"message": "Añade un encabezado al diálogo"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "form[method='dialog']",
|
||||
"message": "Add a <kbd><form method=\"dialog\"></kbd> for the buttons"
|
||||
"message": "Añade un <kbd><form method=\"dialog\"></kbd> para los botones"
|
||||
},
|
||||
{
|
||||
"type": "element_count",
|
||||
"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)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||
"id": "html-forms-fieldset",
|
||||
"title": "Fieldsets",
|
||||
"description": "Group form controls with fieldset and legend elements",
|
||||
"description": "Agrupa controles de formulario con elementos fieldset y legend",
|
||||
"mode": "html",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "fieldset-basic",
|
||||
"title": "Grouping with Fieldset",
|
||||
"description": "The <kbd><fieldset></kbd> element groups related form controls together. Add a <kbd><legend></kbd> as the first child to give the group a title.<br><br>This helps with accessibility and visual organization of complex forms.",
|
||||
"task": "Create a form with a fieldset:<br>1. A <kbd><form></kbd> element<br>2. A <kbd><fieldset></kbd> inside<br>3. A <kbd><legend></kbd> saying <code>Personal Info</code><br>4. Two labeled inputs for name and email",
|
||||
"title": "Agrupar con Fieldset",
|
||||
"description": "El elemento <kbd><fieldset></kbd> agrupa controles de formulario relacionados. Añade un <kbd><legend></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": "Crea un formulario con un fieldset:<br>1. Un elemento <kbd><form></kbd><br>2. Un <kbd><fieldset></kbd> dentro<br>3. Un <kbd><legend></kbd> que diga <code>Información Personal</code><br>4. Dos campos etiquetados para nombre y email",
|
||||
"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; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -21,35 +21,35 @@
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "form",
|
||||
"message": "Add a <kbd><form></kbd> element"
|
||||
"message": "Añade un elemento <kbd><form></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "fieldset",
|
||||
"message": "Add a <kbd><fieldset></kbd> inside the form"
|
||||
"message": "Añade un <kbd><fieldset></kbd> dentro del formulario"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "legend",
|
||||
"message": "Add a <kbd><legend></kbd> to title your fieldset"
|
||||
"message": "Añade un <kbd><legend></kbd> para titular tu fieldset"
|
||||
},
|
||||
{
|
||||
"type": "element_count",
|
||||
"value": { "selector": "label", "min": 2 },
|
||||
"message": "Add at least 2 labels"
|
||||
"message": "Añade al menos 2 etiquetas"
|
||||
},
|
||||
{
|
||||
"type": "element_count",
|
||||
"value": { "selector": "input", "min": 2 },
|
||||
"message": "Add at least 2 input fields"
|
||||
"message": "Añade al menos 2 campos de entrada"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "fieldset-textarea",
|
||||
"title": "Adding Textarea",
|
||||
"description": "The <kbd><textarea></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.",
|
||||
"task": "Create a contact form:<br>1. A <kbd><fieldset></kbd> with <kbd><legend></kbd> <code>Contact Us</code><br>2. A labeled <kbd><input></kbd> for email<br>3. A labeled <kbd><textarea></kbd> for the message<br>4. A submit <kbd><button></kbd>",
|
||||
"title": "Añadiendo Textarea",
|
||||
"description": "El elemento <kbd><textarea></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": "Crea un formulario de contacto:<br>1. Un <kbd><fieldset></kbd> con <kbd><legend></kbd> <code>Contáctanos</code><br>2. Un <kbd><input></kbd> etiquetado para email<br>3. Un <kbd><textarea></kbd> etiquetado para el mensaje<br>4. Un <kbd><button></kbd> de envío",
|
||||
"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; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -60,35 +60,35 @@
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "fieldset",
|
||||
"message": "Add a <kbd><fieldset></kbd> element"
|
||||
"message": "Añade un elemento <kbd><fieldset></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "legend",
|
||||
"message": "Add a <kbd><legend></kbd> element"
|
||||
"message": "Añade un elemento <kbd><legend></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "textarea",
|
||||
"message": "Add a <kbd><textarea></kbd> for the message"
|
||||
"message": "Añade un <kbd><textarea></kbd> para el mensaje"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "button",
|
||||
"message": "Add a submit button"
|
||||
"message": "Añade un botón de envío"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "input",
|
||||
"message": "Add an input field for email"
|
||||
"message": "Añade un campo de entrada para el email"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "fieldset-multiple",
|
||||
"title": "Multiple Fieldsets",
|
||||
"description": "Complex forms can use multiple <kbd><fieldset></kbd> elements to organize different sections.<br><br>This improves usability for long forms like registration or checkout pages.",
|
||||
"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",
|
||||
"title": "Múltiples Fieldsets",
|
||||
"description": "Los formularios complejos pueden usar múltiples elementos <kbd><fieldset></kbd> para organizar diferentes secciones.<br><br>Esto mejora la usabilidad en formularios largos como registro o checkout.",
|
||||
"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": "",
|
||||
"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": "",
|
||||
@@ -99,27 +99,27 @@
|
||||
{
|
||||
"type": "element_count",
|
||||
"value": { "selector": "fieldset", "min": 2 },
|
||||
"message": "Create at least 2 fieldsets"
|
||||
"message": "Crea al menos 2 fieldsets"
|
||||
},
|
||||
{
|
||||
"type": "element_count",
|
||||
"value": { "selector": "legend", "min": 2 },
|
||||
"message": "Add a legend to each fieldset"
|
||||
"message": "Añade un legend a cada fieldset"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "textarea",
|
||||
"message": "Add a textarea for the bio"
|
||||
"message": "Añade un textarea para la bio"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "button",
|
||||
"message": "Add a submit button"
|
||||
"message": "Añade un botón de envío"
|
||||
},
|
||||
{
|
||||
"type": "element_count",
|
||||
"value": { "selector": "input", "min": 2 },
|
||||
"message": "Add at least 2 input fields"
|
||||
"message": "Añade al menos 2 campos de entrada"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,125 +1,42 @@
|
||||
{
|
||||
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||
"id": "html-tables",
|
||||
"title": "HTML Tables",
|
||||
"description": "Create structured data tables with headers and captions",
|
||||
"title": "Tablas HTML",
|
||||
"description": "Crea tablas de datos estructuradas con marcado semántico",
|
||||
"mode": "html",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "table-basic",
|
||||
"title": "Basic Table Structure",
|
||||
"description": "Tables use <kbd><table></kbd> with <kbd><tr></kbd> for rows. Inside rows, use <kbd><th></kbd> for headers and <kbd><td></kbd> for data cells.<br><br>The <kbd><caption></kbd> element provides an accessible title for the table.",
|
||||
"task": "Create a simple table with:<br>1. A <kbd><caption></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",
|
||||
"title": "Tablas de datos",
|
||||
"description": "Las tablas muestran datos estructurados en filas y columnas. Usa <kbd><table></kbd> como contenedor, <kbd><tr></kbd> para filas, <kbd><th></kbd> para celdas de encabezado y <kbd><td></kbd> para celdas de datos.<br><br>Añade <kbd><caption></kbd> para un título accesible que describa el contenido de la tabla.",
|
||||
"task": "Crea una tabla de precios:<br>1. Un <kbd><caption></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": "",
|
||||
"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": "",
|
||||
"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",
|
||||
"validations": [
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "table",
|
||||
"message": "Add a <kbd><table></kbd> element"
|
||||
"message": "Añade un elemento <kbd><table></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "caption",
|
||||
"message": "Add a <kbd><caption></kbd> for the table title"
|
||||
"message": "Añade un <kbd><caption></kbd> para el título de la tabla"
|
||||
},
|
||||
{
|
||||
"type": "element_count",
|
||||
"value": { "selector": "th", "min": 2 },
|
||||
"message": "Add at least 2 header cells (th)"
|
||||
"message": "Añade celdas de encabezado (<kbd><th></kbd>) para Plan y Price"
|
||||
},
|
||||
{
|
||||
"type": "element_count",
|
||||
"value": { "selector": "tr", "min": 3 },
|
||||
"message": "Add at least 3 rows (1 header + 2 data rows)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "table-thead-tbody",
|
||||
"title": "Table Head & Body",
|
||||
"description": "Use <kbd><thead></kbd> to group header rows and <kbd><tbody></kbd> to group data rows. This helps browsers and assistive technology understand the table structure.<br><br>You can also use <kbd><tfoot></kbd> for footer rows like totals.",
|
||||
"task": "Create a structured table:<br>1. A <kbd><caption></kbd> with <code>Monthly Sales</code><br>2. A <kbd><thead></kbd> with Month and Revenue headers<br>3. A <kbd><tbody></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><table></kbd> element"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "caption",
|
||||
"message": "Add a <kbd><caption></kbd> element"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "thead",
|
||||
"message": "Add a <kbd><thead></kbd> for the header section"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "tbody",
|
||||
"message": "Add a <kbd><tbody></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><tfoot></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><caption></kbd> with <code>Order Summary</code><br>2. A <kbd><thead></kbd> with Item and Price headers<br>3. A <kbd><tbody></kbd> with 2 items<br>4. A <kbd><tfoot></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><table></kbd> element"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "caption",
|
||||
"message": "Add a <kbd><caption></kbd> element"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "thead",
|
||||
"message": "Add a <kbd><thead></kbd> section"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "tbody",
|
||||
"message": "Add a <kbd><tbody></kbd> section"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "tfoot",
|
||||
"message": "Add a <kbd><tfoot></kbd> section for the total"
|
||||
},
|
||||
{
|
||||
"type": "element_count",
|
||||
"value": { "selector": "tbody tr", "min": 2 },
|
||||
"message": "Add at least 2 item rows in tbody"
|
||||
"message": "Añade 3 filas (1 encabezado + 2 filas de datos)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||
"id": "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",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "marquee-basic",
|
||||
"title": "Scrolling Text",
|
||||
"description": "The <kbd><marquee></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!",
|
||||
"task": "Create a simple marquee:<br>1. Add a <kbd><marquee></kbd> element<br>2. Put some text inside like <code>Welcome to my website!</code>",
|
||||
"title": "Texto Desplazable",
|
||||
"description": "El elemento <kbd><marquee></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": "Crea un marquee simple:<br>1. Añade un elemento <kbd><marquee></kbd><br>2. Pon texto dentro como <code>¡Bienvenido a mi sitio web!</code>",
|
||||
"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; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -21,15 +21,15 @@
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "marquee",
|
||||
"message": "Add a <kbd><marquee></kbd> element"
|
||||
"message": "Añade un elemento <kbd><marquee></kbd>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "marquee-direction",
|
||||
"title": "Direction & Behavior",
|
||||
"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)",
|
||||
"task": "Create a bouncing marquee:<br>1. Add a <kbd><marquee></kbd> element<br>2. Set <kbd>behavior=\"alternate\"</kbd> to make it bounce<br>3. Add some fun text",
|
||||
"title": "Dirección y Comportamiento",
|
||||
"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": "Crea un marquee que rebota:<br>1. Añade un elemento <kbd><marquee></kbd><br>2. Pon <kbd>behavior=\"alternate\"</kbd> para hacerlo rebotar<br>3. Añade texto divertido",
|
||||
"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; }",
|
||||
"sandboxCSS": "",
|
||||
@@ -40,20 +40,20 @@
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "marquee",
|
||||
"message": "Add a <kbd><marquee></kbd> element"
|
||||
"message": "Añade un elemento <kbd><marquee></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "attribute_value",
|
||||
"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",
|
||||
"title": "Retro News Ticker",
|
||||
"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.",
|
||||
"task": "Create a news ticker:<br>1. A <kbd><marquee></kbd> with <kbd>direction=\"left\"</kbd><br>2. Set <kbd>scrollamount=\"5\"</kbd> for smooth scrolling<br>3. Add a breaking news headline inside",
|
||||
"title": "Ticker de Noticias Retro",
|
||||
"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": "Crea un ticker de noticias:<br>1. Un <kbd><marquee></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": "",
|
||||
"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": "",
|
||||
@@ -64,17 +64,17 @@
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "marquee",
|
||||
"message": "Add a <kbd><marquee></kbd> element"
|
||||
"message": "Añade un elemento <kbd><marquee></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "attribute_value",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,99 +2,169 @@
|
||||
"$schema": "../../schemas/code-crispies-module-schema.json",
|
||||
"id": "html-svg",
|
||||
"title": "HTML SVG",
|
||||
"description": "Draw scalable vector graphics directly in HTML",
|
||||
"description": "Dibuja gráficos vectoriales escalables directamente en HTML",
|
||||
"mode": "html",
|
||||
"difficulty": "beginner",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "svg-circle",
|
||||
"title": "Drawing Circles",
|
||||
"description": "SVG (Scalable Vector Graphics) lets you draw shapes directly in HTML. The <kbd><svg></kbd> element is the container, with <kbd>width</kbd> and <kbd>height</kbd> attributes.<br><br>Use <kbd><circle></kbd> with <kbd>cx</kbd>, <kbd>cy</kbd> (center) and <kbd>r</kbd> (radius) to draw circles.",
|
||||
"task": "Create an SVG with a circle:<br>1. An <kbd><svg></kbd> with width=\"200\" and height=\"200\"<br>2. A <kbd><circle></kbd> centered at (100,100) with radius 50<br>3. Add a <kbd>fill</kbd> color",
|
||||
"title": "Dibujando círculos",
|
||||
"description": "SVG (Scalable Vector Graphics) permite dibujar formas directamente en HTML. El elemento <kbd><svg></kbd> es el contenedor con atributos <kbd>width</kbd> y <kbd>height</kbd>.<br><br>Usa <kbd><circle></kbd> con <kbd>cx</kbd>, <kbd>cy</kbd> (centro) y <kbd>r</kbd> (radio) para dibujar círculos.",
|
||||
"task": "Crea un SVG con un círculo:<br>1. Un <kbd><svg></kbd> con width=\"200\" y height=\"200\"<br>2. Un <kbd><circle></kbd> centrado en (100,100) con radio 50<br>3. Añade un color <kbd>fill</kbd>",
|
||||
"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); }",
|
||||
"sandboxCSS": "",
|
||||
"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",
|
||||
"validations": [
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "svg",
|
||||
"message": "Add an <kbd><svg></kbd> element"
|
||||
"message": "Añade un elemento <kbd><svg></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "circle",
|
||||
"message": "Add a <kbd><circle></kbd> element inside the SVG"
|
||||
"message": "Añade un elemento <kbd><circle></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",
|
||||
"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",
|
||||
"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",
|
||||
"title": "Rectangles & Lines",
|
||||
"description": "Draw rectangles with <kbd><rect></kbd> using <kbd>x</kbd>, <kbd>y</kbd>, <kbd>width</kbd>, <kbd>height</kbd>.<br><br>Draw lines with <kbd><line></kbd> using <kbd>x1</kbd>, <kbd>y1</kbd> (start) and <kbd>x2</kbd>, <kbd>y2</kbd> (end). Lines need a <kbd>stroke</kbd> color!",
|
||||
"task": "Create an SVG with:<br>1. An <kbd><svg></kbd> (200x150)<br>2. A <kbd><rect></kbd> at position (20,20) with size 80x60<br>3. A <kbd><line></kbd> from (120,30) to (180,90) with a stroke color",
|
||||
"title": "Rectángulos y líneas",
|
||||
"description": "Dibuja rectángulos con <kbd><rect></kbd> usando <kbd>x</kbd>, <kbd>y</kbd>, <kbd>width</kbd>, <kbd>height</kbd>.<br><br>Dibuja líneas con <kbd><line></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": "Crea un SVG con:<br>1. Un <kbd><svg></kbd> (200x150)<br>2. Un <kbd><rect></kbd> en posición (20,20) con tamaño 80x60<br>3. Una <kbd><line></kbd> de (120,30) a (180,90) con color stroke",
|
||||
"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); }",
|
||||
"sandboxCSS": "",
|
||||
"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",
|
||||
"validations": [
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "svg",
|
||||
"message": "Add an <kbd><svg></kbd> element"
|
||||
"message": "Añade un elemento <kbd><svg></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "rect",
|
||||
"message": "Add a <kbd><rect></kbd> element"
|
||||
"message": "Añade un elemento <kbd><rect></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "line",
|
||||
"message": "Add a <kbd><line></kbd> element"
|
||||
"message": "Añade un elemento <kbd><line></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",
|
||||
"title": "Multiple Shapes",
|
||||
"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.",
|
||||
"task": "Create a simple face:<br>1. An <kbd><svg></kbd> (200x200)<br>2. A large <kbd><circle></kbd> for the face<br>3. Two small <kbd><circle></kbd> elements for eyes<br>4. A <kbd><line></kbd> for the smile",
|
||||
"title": "Múltiples formas",
|
||||
"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": "Crea una cara simple:<br>1. Un <kbd><svg></kbd> (200x200)<br>2. Un <kbd><circle></kbd> grande para la cara<br>3. Dos <kbd><circle></kbd> pequeños para los ojos<br>4. Una <kbd><line></kbd> para la sonrisa",
|
||||
"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); }",
|
||||
"sandboxCSS": "",
|
||||
"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",
|
||||
"validations": [
|
||||
{
|
||||
"type": "element_exists",
|
||||
"value": "svg",
|
||||
"message": "Add an <kbd><svg></kbd> element"
|
||||
"message": "Añade un elemento <kbd><svg></kbd>"
|
||||
},
|
||||
{
|
||||
"type": "element_count",
|
||||
"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",
|
||||
"value": "line",
|
||||
"message": "Add a <kbd><line></kbd> for the smile"
|
||||
"message": "Añade una <kbd><line></kbd> para la sonrisa"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user