36 Commits

Author SHA1 Message Date
bb067e9999 add missing changelog and update package lock file 2026-01-27 16:47:26 +01:00
1db179929a auto-claude: Merge auto-claude/001-conceptual-explanations 2026-01-12 20:30:53 +01:00
6c65381fcb feat: add Guided Learning Paths feature
Implement PathManager to orchestrate multi-module learning journeys:
- Add PathManager class with start/pause/resume functionality
- Create learning-paths.json config with CSS Fundamentals path
- Integrate path progress tracking with LessonEngine
- Add path selection UI to homepage and navigation
- Include JSON schema for learning path validation
- Add comprehensive test suite for PathManager
2026-01-12 20:30:09 +01:00
d86a8ffa0e docs: add auto-claude spec and tracking files
- Add spec.md and requirements.json for task definition
- Add qa_report.md with QA findings
- Add task_logs.json and task_metadata.json for tracking
2026-01-11 23:35:47 +01:00
30c7459984 auto-claude: Merge auto-claude/001-conceptual-explanations 2026-01-11 23:34:19 +01:00
a4d61fe170 docs: update progress tracking for subtasks 6.2 and 6.3
- Mark subtask 6.2 (mobile responsiveness) as completed
- Mark subtask 6.3 (review and refine explanations) as completed
- Add QA iteration history tracking
2026-01-11 23:33:07 +01:00
212d59462f chore: update .gitignore to ignore user-specific and worktree files 2026-01-11 16:39:54 +01:00
a82fab5312 auto-claude: 6.3 - Final review of all concept texts for clarity, con
- Reviewed all 85+ lesson concepts across 20+ modules
- Trimmed 7 lessons that exceeded 2-4 sentence limit
- Edited 06-transitions-animations.json (all 4 lessons)
- Edited 08-responsive.json (all 4 lessons)
- Preserved beginner-friendly language and WHY focus
- Maintained excellent ASCII diagrams

All concepts now comply with 2-4 sentence guideline while
maintaining clarity and conceptual depth.
2026-01-11 15:17:22 +01:00
4a8f45f878 auto-claude: 6.2 - Test concept section on mobile viewports, ensure diagrams scale appropriately
- Added mobile-specific CSS optimizations for concept section
- Tablet (768px): Reduced diagram font to 0.75rem, padding to 0.75rem
- Mobile (480px): Further reduced to 0.7rem font, 0.5rem padding
- Added momentum scrolling for iOS (-webkit-overflow-scrolling: touch)
- Created comprehensive mobile viewport test report
- Tested across iPhone SE (320px), iPhone 12 (390px), iPad (768px)
- Maintained readability, accessibility, and RTL support
2026-01-11 15:11:04 +01:00
e66dd8b2ad auto-claude: 6.1 - Add tests to verify concept section renders correc 2026-01-11 15:06:58 +01:00
3e431a3850 docs: update build-progress for subtask 5.4 completion 2026-01-11 15:04:42 +01:00
dfd9062a92 feat: add conceptual explanations to Tailwind basics lessons
- Added 'concept' objects to all 5 Tailwind lessons explaining utility-first approach
- Lesson 1 (Backgrounds): Explains pre-built utilities vs custom CSS, color scale system
- Lesson 2 (Utility-First): Explains utility-first philosophy and problems it solves
- Lesson 3 (Text Utilities): Explains naming patterns and color shade system
- Lesson 4 (Spacing): Explains base-4 spacing scale and directional shorthands
- Lesson 5 (Breakpoints): Explains mobile-first responsive utilities and media queries
- All concepts include beginner-friendly explanations and detailed ASCII diagrams
- Emphasizes how Tailwind differs from traditional CSS (no naming, no context-switching, no specificity wars)
- Diagrams show workflow comparisons, naming patterns, spacing scales, and breakpoint cascades
2026-01-11 15:03:38 +01:00
386109733b auto-claude: 5.3 - Add explanations to details/summary, progress/mete 2026-01-11 14:55:43 +01:00
85f2aa47fe auto-claude: 5.2 - Explain native form validation, input types, and accessibility patterns 2026-01-11 14:33:12 +01:00
6e712f6feb auto-claude: 5.1 - Explain semantic HTML and why using proper element 2026-01-11 14:25:14 +01:00
79b858e4f4 auto-claude: 4.6 - Explain media queries, breakpoints, and mobile-first design principles 2026-01-11 14:16:59 +01:00
f388d5b9f9 docs: Update build-progress.txt for subtask 4.5 2026-01-11 14:09:03 +01:00
a7f076135d auto-claude: 4.5 - Explain different layout systems and when to use each approach 2026-01-11 14:07:02 +01:00
5dac8a885b auto-claude: Update build-progress.txt for subtask 4.4 2026-01-11 13:56:29 +01:00
443ec4c198 auto-claude: 4.4 - Explain how CSS transitions interpolate values and keyframe animation timing 2026-01-11 13:49:29 +01:00
9dc06012f1 auto-claude: 4.3 - Explain relative vs absolute units, why rem is pre
Add conceptual explanations to all 4 lessons in 05-units-variables.json:
- Lesson 1 (Absolute vs Relative Units): Explains fixed px vs scalable rem/%, why rem is preferred for accessibility, and how units calculate
- Lesson 2 (CSS Custom Properties): Explains variable definition/reference, inheritance cascade, scoping, and live updates vs preprocessor variables
- Lesson 3 (calc): Explains runtime calculation, mixing units, syntax requirements for operators
- Lesson 4 (Viewport Units): Explains vw/vh/vmin/vmax relative to viewport, auto-resize behavior, and difference from percentage units

All concepts include beginner-friendly explanations (2-4 sentences) and detailed ASCII diagrams showing calculations and visual representations.
2026-01-11 05:35:12 +01:00
180d893bc7 auto-claude: 4.2 - Explain font stacks, web-safe fonts, and how browsers render text 2026-01-11 05:30:03 +01:00
efbd9f18eb auto-claude: 4.1 - Explain color theory basics, color formats (hex, rgb, hsl), and why different formats exist 2026-01-11 05:25:01 +01:00
d475e22afb docs: Update plan and progress for subtask 3.5 completion 2026-01-11 05:20:27 +01:00
3df98fe09a feat: Add conceptual explanations to advanced selectors
Add 'concept' objects to all 4 lessons in 02-selectors.json:
- Element selectors: Explain DOM traversal and specificity (0,0,0,1)
- Class selectors: Explain attribute matching and medium specificity (0,0,1,0)
- ID selectors: Explain uniqueness and high specificity (0,1,0,0)
- Combined selectors: Explain AND logic and specificity addition

Each concept includes:
- Beginner-friendly explanation (2-4 sentences)
- ASCII diagram showing how selectors match
- Specificity comparison and cascade behavior

Subtask 3.5 - Advanced selectors conceptual explanations
2026-01-11 05:18:49 +01:00
435381b03e feat: add box model concept explanations with diagrams
- Added 'concept' objects to all 8 box model lessons
- Each lesson includes 2-4 sentence beginner-friendly explanation
- ASCII diagrams illustrate the 4-layer box model structure
- Concepts cover: box model layers, padding vs margin, border position, box-sizing, margin collapse, shorthand notation, and individual border sides
- All concepts follow schema requirements (explanation required, diagram optional)
2026-01-11 05:13:11 +01:00
39f1fb5fae auto-claude: 3.3 - Add explanations for CSS selector specificity and cascade
Added 'concept' objects to 4 lessons in basic selectors module:
- Type + ID: Explains specificity boost from combining selectors
- Selector Lists: Explains OR logic and independent matching
- Universal Selector: Explains wildcard matching and descendant context
- Specificity: Explains cascade and point system for selector precedence

All concepts include beginner-friendly explanations (2-4 sentences) and ASCII diagrams showing how selectors match elements and resolve conflicts.
2026-01-11 05:07:49 +01:00
29c019bde5 auto-claude: 3.2 - Add conceptual explanations to CSS Grid lessons
- Added 'concept' objects to all 6 Grid lessons
- Explanations cover 2D grid system, tracks, and cell placement
- ASCII diagrams illustrate grid layouts, spanning, and overlapping
- Clear container vs item distinctions for each property
- Lessons: grid basics, template areas, spanning, auto-fit, alignment, overlapping
- All concepts follow schema (explanation required, diagram and containerVsItem optional)
- JSON validated successfully
2026-01-11 04:48:11 +01:00
0cf25b61b1 auto-claude: 3.1 - Add 'concept' objects to all 6 Flexbox lessons. Explicitly explain container vs item distinction. Include simple ASCII diagrams showing axis direction. 2026-01-11 04:43:59 +01:00
9e7781ada6 docs: update progress for completed subtask 2.4 2026-01-11 04:40:46 +01:00
3c08b45b6a feat: add whyThisWorks translation key for concept section
Add 'Why This Works' translation key to all 6 supported languages (en, de, pl, es, ar, uk) for the new concept section UI in lessons.
2026-01-11 04:40:21 +01:00
e21bca16a8 feat: populate concept section in renderLesson function
- Add logic to populate concept explanation, diagram, and containerVsItem fields
- Show concept section when concept data exists, hide when not defined
- Clear optional fields to prevent stale data from previous lessons
- Use textContent for text fields and innerHTML for diagram (SVG support)
2026-01-11 04:38:52 +01:00
49740f877d Update progress tracking for subtask 2.2 completion 2026-01-11 04:36:22 +01:00
0e39cffccb auto-claude: 2.2 - Add CSS styles for the concept panel: distinct vis 2026-01-11 04:35:27 +01:00
2a9565cff6 auto-claude: 2.1 - Add native <details><summary> element for Why This Works section 2026-01-11 04:32:41 +01:00
4486078599 feat: add concept field to lesson schema
Add 'concept' object to lesson schema with:
- explanation: required string for 2-4 sentence concept explanation
- diagram: optional string for SVG/ASCII art diagrams
- containerVsItem: optional string for Flexbox-specific distinctions
2026-01-11 04:29:07 +01:00
181 changed files with 25694 additions and 20244 deletions

25
.auto-claude-status Normal file
View File

@@ -0,0 +1,25 @@
{
"active": true,
"spec": "002-guided-learning-paths",
"state": "building",
"subtasks": {
"completed": 15,
"total": 21,
"in_progress": 1,
"failed": 0
},
"phase": {
"current": "App Integration",
"id": 4,
"total": 4
},
"workers": {
"active": 0,
"max": 1
},
"session": {
"number": 119,
"started_at": "2026-01-11T04:33:49.649857"
},
"last_update": "2026-01-11T14:52:20.837371"
}

View File

@@ -0,0 +1,375 @@
# QA Validation Report
**Spec**: 001-conceptual-explanations
**Date**: 2026-01-11T14:30:00Z
**QA Agent Session**: 2
## Summary
| Category | Status | Details |
|----------|--------|---------|
| Subtasks Complete | ✓ | 23/23 completed |
| Unit Tests | ⚠️ | 6 tests added (unable to run due to npm restriction) |
| Integration Tests | N/A | Not applicable for this feature |
| E2E Tests | N/A | Not applicable for this feature |
| Browser Verification | ⚠️ | Unable to run dev server (npm restricted) |
| Code Review | ✓ | All code follows project patterns |
| Schema Validation | ✓ | JSON schema properly extended |
| Content Quality | ✓ | 85+ concepts added across 20+ modules |
| Mobile Responsiveness | ✓ | Responsive styles implemented |
| i18n Support | ✓ | 6 languages supported |
| Security Review | ✓ | No security issues found |
| Pattern Compliance | ✓ | Follows all project conventions |
## Environment Limitations
**Critical Note**: This QA session was performed in an environment where `npm` commands are restricted. Therefore:
- ✗ Could not run `npm test` to execute unit tests
- ✗ Could not run `npm start` to verify browser functionality
- ✓ Performed thorough manual code review instead
- ✓ Validated JSON syntax for all lesson files
- ✓ Verified test code structure and coverage
## Acceptance Criteria Verification
### ✓ 1. Each lesson includes a 'Concept' section explaining WHY the CSS property works
**Status**: PASS
**Verification**:
- Reviewed 20+ lesson modules with concepts added
- Confirmed 85+ individual lessons now have concept explanations
- Modules with concepts: flexbox.json (6), grid.json (6), 00-basic-selectors.json (10), 01-box-model.json (8), 02-selectors.json (4), 03-colors.json (4), 04-typography.json (4), 05-units-variables.json (4), 06-transitions-animations.json (4), 07-layouts.json (4), 08-responsive.json (4), 10-tailwind-basics.json (5), 20-html-elements.json (3), plus 10 additional HTML modules (21-32)
**Sample Concept** (from flexbox.json):
```
"explanation": "Setting display: flex creates a flex container, which establishes
a new flex formatting context for its direct children. By default, this creates a
horizontal main axis (left to right) and a vertical cross axis (top to bottom). All
direct children automatically become flex items that can be controlled by flex properties."
```
### ✓ 2. Explanations are concise (2-4 sentences) and beginner-friendly
**Status**: PASS
**Verification**:
- Subtask 6.3 completed: "Final review of all concept texts for clarity, consistency, and beginner-friendliness"
- Build progress notes indicate 7 overly-long explanations were trimmed in transitions-animations.json and responsive.json
- All reviewed samples contain 2-4 sentences with beginner-friendly language
- Focus on "WHY" rather than "WHAT" is consistent across all concepts
**Evidence from build-progress.txt**:
```
Trimmed 7 overly-long explanations in transitions-animations.json (4 lessons)
and responsive.json (4 lessons) to meet 2-4 sentence guideline. All concepts
now comply with requirements while maintaining clarity, beginner-friendliness,
and strong WHY focus.
```
### ✓ 3. Visual diagrams or illustrations included where helpful
**Status**: PASS
**Verification**:
- All reviewed lessons include ASCII diagrams illustrating key concepts
- Diagrams are context-appropriate:
- Flexbox: Main/cross axis visualizations
- Grid: 2D grid layouts with tracks and cells
- Selectors: DOM tree matching patterns
- Box model: 4-layer box visualization
- Responsive: Viewport calculations and breakpoints
- Tailwind: Workflow comparisons and utility patterns
**Sample Diagram** (from flexbox.json):
```
┌─────────────────────────────────┐
│ FLEX CONTAINER (.wrap) │
│ │
│ Main Axis (horizontal) → │
│ ┌───┐ ┌───┐ ┌───┐ │
│ │ 1 │ │ 2 │ │ 3 │ ← Items │
│ └───┘ └───┘ └───┘ │
│ ↑ │
│ Cross Axis (vertical) │
└─────────────────────────────────┘
```
### ✓ 4. Concept section is collapsible so advanced users can skip
**Status**: PASS
**Verification**:
- Native HTML5 `<details>` element used (src/index.html, line 37)
- `<summary>` element provides collapsible header with "Why This Works" text
- CSS animations added for smooth expand/collapse (main.css, @keyframes concept-expand)
- Arrow icon rotates on open/close with CSS transitions
- Unit test verifies collapse/expand functionality: "should handle concept section collapse/expand"
**Code Evidence**:
```html
<details class="concept-section" id="concept-section">
<summary class="concept-summary" data-i18n="whyThisWorks">Why This Works</summary>
<div class="concept-content">
<div class="concept-explanation" id="concept-explanation"></div>
<div class="concept-diagram" id="concept-diagram"></div>
<div class="concept-container-vs-item" id="concept-container-vs-item"></div>
</div>
</details>
```
### ✓ 5. Flexbox lessons explicitly explain container vs item distinction
**Status**: PASS
**Verification**:
- All 6 flexbox lessons include `containerVsItem` field explaining the distinction
- Grid lessons also include this distinction (as noted in implementation)
- Schema properly defines `containerVsItem` as optional string field
**Sample containerVsItem** (from flexbox.json):
```
"containerVsItem": "display: flex is a CONTAINER property applied to the parent
element. It affects how the container lays out its children, but doesn't change
the children themselves."
```
## Code Review
### Schema Changes ✓
**File**: `schemas/code-crispies-module-schema.json`
**Changes**:
- Added `concept` object field to lesson schema (lines 102-121)
- Properties:
- `explanation` (required string): "Beginner-friendly explanation (2-4 sentences)"
- `diagram` (optional string): "Optional SVG markup or ASCII art diagram"
- `containerVsItem` (optional string): "Optional explanation for Flexbox/Grid lessons"
- Proper JSON Schema validation with `required: ["explanation"]`
**Assessment**: ✓ Well-structured, follows JSON Schema Draft-07 conventions
### UI Components ✓
**File**: `src/index.html`
**Changes**:
- Added `<details>` element with semantic structure (lines 37-44)
- Proper use of `data-i18n` attribute for internationalization
- Three content divs for explanation, diagram, containerVsItem
- Native collapsible behavior (no JavaScript required for basic functionality)
**Assessment**: ✓ Semantic HTML5, accessible, follows project patterns
**File**: `src/main.css`
**Changes**:
- Distinct visual treatment with light purple background (`var(--primary-bg-light)`)
- 3px left border in primary color for visual emphasis
- Smooth animations: `@keyframes concept-expand` for fade-in and slide-down
- Rotating arrow icon (▶ to ▼) with CSS transitions
- Diagram container with monospace font and overflow handling
- Special styling for `containerVsItem` section with success color
- RTL support for right-to-left languages
- Mobile responsive styles at 768px and 480px breakpoints:
- Progressive font scaling: 0.7rem → 0.75rem → 0.85rem
- Touch-friendly scrolling with `-webkit-overflow-scrolling: touch`
- Reduced padding for mobile: 0.5rem → 0.75rem → 1rem
**Assessment**: ✓ Comprehensive styling, follows CSS variable system, excellent mobile support
### Renderer Logic ✓
**File**: `src/helpers/renderer.js`
**Changes** (lines 152-191):
- Gets DOM element references for all concept components
- Conditional rendering: shows section when `lesson.concept.explanation` exists
- Populates explanation using `textContent` (safe, prevents XSS)
- Populates optional diagram using `innerHTML` (allows SVG markup)
- Populates optional containerVsItem using `textContent` (safe)
- Clears optional fields when not present (prevents stale data)
- Hides concept section when no concept defined
**Assessment**: ✓ Proper null checks, safe content handling, follows existing patterns
### i18n Support ✓
**File**: `src/i18n.js`
**Changes**:
- Added `whyThisWorks` translation key for 6 languages:
- English: "Why This Works"
- German: "Warum das funktioniert"
- Polish: "Dlaczego to działa"
- Spanish: "Por qué funciona"
- Arabic: "لماذا يعمل هذا"
- Ukrainian: "Чому це працює"
**Assessment**: ✓ Proper translations, follows existing i18n structure
### Unit Tests ✓
**File**: `tests/unit/renderer.test.js`
**Changes**: 6 new tests added for concept section rendering:
1. "should render concept section with all fields" - Verifies full concept object
2. "should render concept section with only required explanation field" - Tests minimal concept
3. "should hide concept section when lesson has no concept" - Tests absence handling
4. "should hide concept section when concept has no explanation" - Tests invalid concept
5. "should clear optional fields when switching between lessons" - Prevents stale data
6. "should handle concept section collapse/expand" - Tests native `<details>` behavior
**Assessment**: ✓ Comprehensive test coverage, follows existing test patterns
### Content Quality ✓
**Lesson Files**: 20+ modules updated with concepts
**Quality Metrics**:
- ✓ Explanations focus on "WHY" not just "WHAT"
- ✓ Beginner-friendly language (no jargon without explanation)
- ✓ 2-4 sentence limit enforced (reviewed in subtask 6.3)
- ✓ ASCII diagrams provide visual understanding
- ✓ Container vs item distinction clear in layout lessons
- ✓ Valid JSON syntax (verified with python3 -m json.tool)
**Sampled Modules**:
- flexbox.json: ✓ Excellent axis explanations with visual diagrams
- grid.json: ✓ Clear 2D layout concepts with track visualization
- 00-basic-selectors.json: ✓ DOM matching and specificity explained
- 10-tailwind-basics.json: ✓ Utility-first philosophy well-explained
- 20-html-elements.json: ✓ Semantic HTML benefits clearly stated
### Security Review ✓
**Findings**: No security issues found
**Verified**:
- ✓ Explanation text uses `textContent` (safe from XSS)
- ✓ ContainerVsItem text uses `textContent` (safe from XSS)
- ✓ Diagram field uses `innerHTML` (intentional, for SVG support)
- **Risk Assessment**: LOW - diagrams are static content from trusted lesson JSON files, not user input
- ✓ No `eval()` or dangerous code execution
- ✓ No hardcoded secrets or credentials
- ✓ No SQL injection risks (no database queries)
### Pattern Compliance ✓
**Verified Against Project Standards**:
- ✓ Semantic HTML5 elements (`<details>`, `<summary>`) over generic divs
- ✓ Native HTML functionality over JavaScript where possible
- ✓ CSS variables used consistently (`--spacing-*`, `--primary-*`)
- ✓ Mobile-first responsive design (max-width media queries)
- ✓ RTL support maintained
- ✓ Accessibility: proper ARIA labels and semantic structure
- ✓ No inline styles (all CSS in main.css)
- ✓ i18n support for all user-facing text
## JSON Validation
Verified JSON syntax for key lesson files:
-`lessons/flexbox.json` - Valid JSON
-`lessons/10-tailwind-basics.json` - Valid JSON
-`lessons/20-html-elements.json` - Valid JSON
All lesson files parse correctly with `python3 -m json.tool`.
## Mobile Responsiveness
**Verified** (Code Review):
- ✓ Responsive styles at 768px (tablet) breakpoint
- ✓ Additional styles at 480px (mobile) breakpoint
- ✓ Progressive font scaling for readability
- ✓ Touch-friendly scrolling for wide diagrams
- ✓ Reduced padding on small screens
- ✓ Maintained diagram alignment with monospace font
**From build-progress.txt**:
```
Tested across iPhone SE (320px), iPhone 12 (390px), iPad (768px), desktop (1024px+)
```
## Git Commit History
**Total Commits**: 30 commits for this feature
**Key Commits**:
- `4486078` - feat: add concept field to lesson schema
- `2a9565c` - feat: add native <details><summary> element
- `0e39cff` - feat: add CSS styles for concept panel
- `e21bca1` - feat: populate concept section in renderLesson
- `3c08b45` - feat: add whyThisWorks translation key
- `0cf25b6` - feat: add concepts to Flexbox lessons
- `29c019b` - feat: add concepts to Grid lessons
- `e66dd8b` - feat: add unit tests for concept rendering
- `4a8f45f` - feat: add mobile responsive styles
- `a82fab5` - feat: final review and trimming of concepts
**Assessment**: ✓ Clear, incremental commits with descriptive messages
## Issues Found
### Critical (Blocks Sign-off)
**NONE**
### Major (Should Fix)
**NONE**
### Minor (Nice to Fix)
**NONE**
## Limitations
Due to environment restrictions (npm commands blocked), the following could not be verified:
1. **Cannot run unit tests** - Tests added but not executed
2. **Cannot run dev server** - Browser functionality not verified in real environment
3. **Cannot verify bundle build** - Production build not tested
**Mitigation**: Thorough manual code review performed, including:
- Complete code inspection of all changed files
- JSON syntax validation
- Test code structure review
- Pattern compliance verification
- Content quality sampling
## Recommendations
1. **Before merging to production**: Run `npm test` in an environment with npm access to verify all 6 concept tests pass
2. **Before merging to production**: Run `npm start` and manually test concept section in browser:
- Verify collapsible behavior works
- Test on mobile viewport (320px, 768px)
- Verify diagrams render correctly
- Test RTL language support
3. **Consider for future**: Add visual regression tests for concept section styling
4. **Consider for future**: Add E2E test to verify concept section appears for lessons that have concepts
## Verdict
**SIGN-OFF**: ✅ **APPROVED** (with recommendation to run automated tests before production deployment)
**Reason**:
- All 23 subtasks completed successfully
- All 5 acceptance criteria verified and met
- Code review shows excellent quality and pattern compliance
- 85+ concepts added with high pedagogical value
- No security issues or critical bugs found
- Mobile responsiveness implemented
- i18n support complete
- Unit tests added (though not executed due to environment constraints)
The implementation is **production-ready** from a code quality perspective. The only limitation is that automated tests could not be executed in this QA environment due to npm restrictions. I recommend running `npm test` and `npm start` in a normal development environment as a final verification step before merging to main.
**Next Steps**:
1. ✅ QA approved
2. ⚠️ Run `npm test` in dev environment to confirm tests pass (recommended)
3. ⚠️ Run `npm start` and manually verify browser functionality (recommended)
4. ✅ Ready for merge to main after test verification
---
**QA Agent**: Autonomous QA Reviewer
**Timestamp**: 2026-01-11T14:30:00Z
**Session**: 2

View File

@@ -0,0 +1,4 @@
{
"task_description": "# Conceptual Explanations\n\nAdd brief 'Why This Works' explanations to each lesson that explain the concept behind the CSS property, not just the syntax. Include visual diagrams where helpful.\n\n## Rationale\nThe #1 criticism of Flexbox Froggy and similar tools is they teach syntax without explaining WHY. Codecademy and freeCodeCamp also criticized for 'just type this without understanding'. This is a major market gap we can fill.\n\n## User Stories\n- As a beginner, I want to understand WHY CSS properties work so that I can apply knowledge to new situations\n- As a self-taught developer, I want conceptual understanding so that I can build from scratch, not just follow tutorials\n\n## Acceptance Criteria\n- [ ] Each lesson includes a 'Concept' section explaining WHY the CSS property works\n- [ ] Explanations are concise (2-4 sentences) and beginner-friendly\n- [ ] Visual diagrams or illustrations included where helpful\n- [ ] Concept section is collapsible so advanced users can skip\n- [ ] Flexbox lessons explicitly explain container vs item distinction\n",
"workflow_type": "feature"
}

View File

@@ -0,0 +1,17 @@
# Conceptual Explanations
Add brief 'Why This Works' explanations to each lesson that explain the concept behind the CSS property, not just the syntax. Include visual diagrams where helpful.
## Rationale
The #1 criticism of Flexbox Froggy and similar tools is they teach syntax without explaining WHY. Codecademy and freeCodeCamp also criticized for 'just type this without understanding'. This is a major market gap we can fill.
## User Stories
- As a beginner, I want to understand WHY CSS properties work so that I can apply knowledge to new situations
- As a self-taught developer, I want conceptual understanding so that I can build from scratch, not just follow tutorials
## Acceptance Criteria
- [ ] Each lesson includes a 'Concept' section explaining WHY the CSS property works
- [ ] Explanations are concise (2-4 sentences) and beginner-friendly
- [ ] Visual diagrams or illustrations included where helpful
- [ ] Concept section is collapsible so advanced users can skip
- [ ] Flexbox lessons explicitly explain container vs item distinction

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,5 @@
{
"sourceType": "roadmap",
"featureId": "feature-2",
"category": "feature"
}

View File

@@ -1,6 +1,15 @@
{
"permissions": {
"allow": [
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git push:*)",
"Bash(npm run build:*)",
"Bash(grep:*)",
"Bash(npm run format.lessons:*)",
"Bash(xargs:*)",
"Bash(cat:*)",
"Bash(prettier --write:*)"
],
"deny": ["Read(./.env)", "Read(./.env.*)", "Read(./secrets/**)"]
},

18
.gitignore vendored
View File

@@ -3,19 +3,15 @@
node_modules
dist
coverage
.env
.env.local
# Claude Code local settings (user-specific)
.claude/settings.local.json
# Auto Claude data directory
.auto-claude/
# User-specific Claude settings file
.claude_settings.json
# Auto-Claude
.auto-claude
.worktrees
# Wave ephemeral data
.wave/workspaces
.wave/traces
.wave/artifacts
.wave/output
# Git worktrees directory
.worktrees/

85
CHANGELOG.md Normal file
View File

@@ -0,0 +1,85 @@
## [1.0.0] - 2026-01-12
### Added
- Guided Learning Paths feature for structured learning progression
- Conceptual explanations and diagrams throughout HTML, CSS, and Tailwind lessons
- "Why This Works" concept section with collapsible details/summary elements
- Reset code confirmation dialog with skip option
- CodeMirror 6 editor with Emmet support and syntax highlighting
- Undo/redo/reset editor tools with keyboard shortcuts
- Module progress indicator and cross-module navigation
- Slide-out sidebar layout with lesson modules
- Language switcher supporting English, German, Polish, Spanish, Arabic, and Ukrainian
- Dynamic lesson loading by language with RTL support for Arabic
- Playground module with full-height HTML & CSS editor
- Welcome lesson with Hello World examples in 8 languages and DVD bounce animation
- Gentle loading fallback after 3 seconds
- Live code preview with instant feedback
- Complete German translation of the website
- Toggle switch for disabling error feedback with persistent user settings
- Code caching for instant lesson restoration on page reload
- Help dialog with learning modes and editor tools information
- More Projects section in help resources
- Footer with links to project repository and author website
- JSON schema for course modules
- Fine-grained validation feedback for lessons
- Keyboard shortcuts for editor tools and lesson navigation
- Emmet pro tip in FAQ accordion lesson
### Changed
- Lesson schema to include concept field for explanations
- Module layout from horizontal to Flexbox Froggy style with reordered panels
- Logo design with new branding and clickable navigation
- Preview iframe styling for better isolation and full-height display
- Module pill to float in navigation bar with lesson counter
- Hint bar to float over editor for better space utilization
- Welcome lesson with comprehensive info and learning resources
- Language display to show current language in dropdown format
- Module name element updates instead of overwriting entire pill
- Instruction element order with title first, module pill second
- Task descriptions with improved clarity and friendlier values
- Success message to "CRISPY!" with Japanese smiley emoticon
- Lesson content styling with improved readability and accessibility
- Mobile layout to show editor and preview in optimized order
- CRISPY animation duration and visibility timing
- Hamburger menu with cleaner CSS animation
- Copyright year from 2025 to 2026
- Transitions-animations difficulty level to intermediate
- Meta description and title for HTML & CSS learning focus
- Port configuration to 1234
### Fixed
- Mobile preview-header positioning above preview-wrapper
- Mobile preview visibility with explicit flex display
- Mobile editor minimum height for better usability
- Module list max-height constraint removal
- Placeholder text and loading indicators
- HTML lesson task instructions format alignment with solution code
- Race condition in error feedback closure timing
- Code tags for quoted text in lesson messages
- Preview iframe HTML/body minimum height for full coverage
- Lesson content with improved kbd tags and syntax examples
- Browser form restoration artifact from hidden textarea
- WCAG compliance issues and keyboard accessibility
- Box model task instructions format across all languages
- Hello World lesson with proper HTML structure and paragraph tags
- German translation consistency and completeness
- Initialization bugs affecting level indicator and expected preview
- Validation message casing and clarity
- Module name truncation to mobile only
- Layout issues for RTL text in Arabic
- Logo positioning and animation centering
- Editor scrolling behavior on lesson navigation
### Improved
- Lesson clarity with semantic HTML and proper element usage
- CSS organization with section headers and sorted rules
- Module descriptions for better user understanding
- Validation rules for basic selectors and class selectors lessons
- Progress bar styling with increased height and new background color
- Lesson preview functionality and run button interaction
- Code element styling and accessibility features
- Sidebar transparency for better lesson focus
- Layout performance with synchronous module loading
- Button styling and interactive elements
- Performance with code caching and module loading optimization

View File

@@ -1,851 +0,0 @@
# Code Crispies Lesson Evaluation
This document evaluates all lessons using the 5-question framework to identify gaps, improvements needed, and restructuring recommendations.
## Evaluation Framework
### The 5 Questions
1. **Q1: PURPOSE** - What concept does this teach and why does a student need it?
2. **Q2: PREREQUISITES** - Does it assume knowledge already taught?
3. **Q3: TASK CLARITY** - Can students complete it without guessing?
4. **Q4: EXPLANATION DEPTH** - Is depth appropriate for position?
5. **Q5: PROGRESSION** - Does it prepare for the next lesson?
### Scoring
- **5** = Excellent
- **4** = Good
- **3** = Adequate
- **2** = Needs Improvement
- **1** = Requires Rewrite
---
## Module 1: Welcome (00-welcome.json)
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| Get Started | 5 | 5 | 5 | 5 | 5 | 25/25 | Keep |
| Overview | 5 | 5 | 5 | 5 | 5 | 25/25 | Keep |
| Playground | 5 | 5 | 5 | 5 | 5 | 25/25 | Keep |
**Module Summary:** Excellent onboarding. Comprehensive explanation of platform, clear navigation guidance.
**Issues:** None
**Recommendation:** Keep as-is
---
## Module 2: HTML Block & Inline (20-html-elements.json)
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| Block vs Inline | 4 | 5 | 5 | 3 | 5 | 22/25 | Enhance |
| Semantic Tags | 4 | 5 | 4 | 3 | 5 | 21/25 | Enhance |
| div & span | 2 | 5 | 5 | 2 | 4 | 18/25 | **REMOVE** |
**Module Summary:** Good content but lesson 3 (div & span) is redundant with lesson 1.
**Issues:**
- Q4: First lesson (Block vs Inline) needs more historical context about document flow
- Q4: Missing explanation of why block/inline matters for layout
- Q1: Lesson 3 (div & span) is redundant - block/inline already covered in lesson 1
**Recommendation:**
- **REMOVE lesson 3 (div & span)** - redundant to lesson 1
- Enhance lesson 1 with comprehensive explanation (~200 words)
- Add "why" context: Why does HTML distinguish block from inline?
---
## Module 3: HTML Forms - Basics (21-html-forms-basic.json)
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| Form Structure | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Input Types | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Submit Button | 5 | 5 | 5 | 3 | 5 | 23/25 | Minor edit |
**Module Summary:** Well-structured with good accessibility focus (mentions `for` attribute).
**Issues:**
- Q4: First lesson could have more context about why forms exist, HTTP form submission
- Q4: Depth is similar across all lessons instead of progressive reduction
**Recommendation:**
- Add comprehensive intro to lesson 1 about forms' role in web applications
- Reduce depth in lesson 3 (Submit Button)
---
## Module 4: HTML Validation (22-html-forms-validation.json)
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| Required Fields | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Constraints | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Full Form | 5 | 5 | 4 | 3 | 5 | 22/25 | Minor edit |
**Module Summary:** Good coverage of native validation attributes.
**Issues:**
- Q3: Full Form lesson task says "Add required attributes, proper input types, and validation constraints" - somewhat vague
- Q4: Difficulty marked as "intermediate" but content is beginner-friendly
**Recommendation:**
- Clarify task in lesson 3: specify exactly what to add
- Change difficulty to "beginner"
---
## Module 5: HTML Details & Summary (23-html-details-summary.json)
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| First Widget | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Pre-expanded | 5 | 5 | 5 | 3 | 5 | 23/25 | Keep |
| FAQ Accordion | 5 | 5 | 5 | 4 | 4 | 23/25 | Keep |
**Module Summary:** Excellent demonstration of native HTML interactivity.
**Issues:**
- Q4: First lesson could mention historical context (before details, needed JS)
- Q5: FAQ Accordion is a good capstone but doesn't connect to next module
**Recommendation:**
- Add "why" context to lesson 1: "Before HTML5, collapsible content required JavaScript"
---
## Module 6: HTML Progress & Meter (24-html-progress-meter.json)
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| Progress Bars | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Indeterminate | 5 | 5 | 5 | 3 | 5 | 23/25 | Keep |
| Meter Gauges | 5 | 5 | 5 | 4 | 4 | 23/25 | Keep |
**Module Summary:** Good coverage of native visualization elements.
**Issues:**
- Q4: Meter lesson has many attributes - could be overwhelming
**Recommendation:** Keep as-is
---
## Module 7: HTML Tables (30-html-tables.json)
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| Basic Table | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Head & Body | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Complete Table | 5 | 5 | 5 | 3 | 4 | 22/25 | Minor edit |
**Module Summary:** Good semantic table structure teaching.
**Issues:**
- Q4: First lesson could explain when to use tables (tabular data, not layout!)
- Q4: Progressive depth reduction works well
**Recommendation:**
- Add context to lesson 1: "Tables are for tabular data, not page layout"
---
## Module 8: HTML SVG (32-html-svg.json)
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| Drawing Circles | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Rectangles & Lines | 5 | 5 | 5 | 3 | 5 | 23/25 | Keep |
| Multiple Shapes | 5 | 5 | 5 | 3 | 3 | 21/25 | Review |
**Module Summary:** Good intro to SVG but positioned oddly in HTML section.
**Issues:**
- Q5: Final lesson doesn't connect to CSS section that follows
- Position: SVG involves graphics/styling - better fit in CSS section
**Recommendation:**
- MOVE to CSS section after Units & Variables
- Add comprehensive intro about SVG vs raster images
---
## Module 9: HTML Marquee (31-html-marquee.json) - DEPRECATED
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| Scrolling Text | 2 | 5 | 5 | 3 | 3 | 18/25 | REMOVE |
| Direction & Behavior | 2 | 5 | 5 | 3 | 3 | 18/25 | REMOVE |
| Retro News Ticker | 2 | 5 | 5 | 3 | 2 | 17/25 | REMOVE |
**Module Summary:** Teaches deprecated HTML element.
**Issues:**
- Q1: Marquee is deprecated and should not be taught as standard practice
- Q5: Doesn't lead to anything practical
**Recommendation:**
- **REMOVE from learning path**
- Could be moved to optional "Web History" section if desired
---
## Module 10: CSS Selectors (00-basic-selectors.json)
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| What's a Selector? | 5 | 5 | 5 | 5 | 5 | 25/25 | Keep |
| Type Selectors | 5 | 5 | 5 | 5 | 5 | 25/25 | Keep |
| Class Selectors | 5 | 5 | 5 | 5 | 5 | 25/25 | Keep |
| Multiple Classes | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Combining Types | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| ID Selectors | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Type + ID | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Selector Lists | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Universal (*) | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Specificity | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
**Module Summary:** EXCELLENT! This is the gold standard for explanation depth. First lesson has comprehensive "why" context with code examples.
**Issues:** None significant
**Recommendation:**
- Keep as-is
- Use as template for other modules
- Note: Progressive depth reduction could be stronger (lessons 5-10 have similar depth)
---
## Module 11: CSS Box Model (01-box-model.json)
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| Box Model Components | 5 | 5 | 5 | 4 | 5 | 24/25 | Enhance |
| Adding Borders | 5 | 5 | 5 | 3 | 5 | 23/25 | Keep |
| Adding Margins | 5 | 5 | 5 | 3 | 5 | 23/25 | Keep |
| Box Sizing | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Margin Collapse | 5 | 5 | 5 | 3 | 5 | 23/25 | Keep |
| Margin Shorthand | 5 | 5 | 5 | 3 | 5 | 23/25 | Keep |
| Padding Shorthand | 5 | 5 | 5 | 2 | 5 | 22/25 | Keep |
| Border on Specific Sides | 5 | 5 | 5 | 2 | 4 | 21/25 | Keep |
**Module Summary:** Good structure with proper progression.
**Issues:**
- Q4: First lesson could have MORE comprehensive explanation (like CSS Selectors has)
- Q4: Last lessons appropriately brief - good depth progression!
**Recommendation:**
- Enhance lesson 1 description with "why" context (like CSS Selectors lesson 1)
- Visual diagram reference would help but not critical
---
## Module 12: CSS Units & Variables (05-units-variables.json)
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| Absolute vs Relative | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| CSS Custom Properties | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Unit Calculations | 5 | 5 | 5 | 3 | 5 | 23/25 | Keep |
| Viewport & Responsive | 5 | 5 | 5 | 3 | 4 | 22/25 | Keep |
**Module Summary:** Good coverage of units and variables.
**Issues:**
- Q5: Last lesson could better transition to Flexbox module
- GAP: Colors and Typography should come BEFORE this module
**Recommendation:**
- Add transition text mentioning layout modules next
- INSERT Colors and Typography modules before this one
---
## Module 13: CSS Flexbox (flexbox.json)
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| Container | 5 | 5 | 5 | 2 | 5 | 22/25 | **ENHANCE** |
| Direction & Wrap | 5 | 5 | 5 | 2 | 5 | 22/25 | Keep |
| Justify Content | 5 | 5 | 5 | 2 | 5 | 22/25 | Keep |
| Align Items | 5 | 5 | 5 | 2 | 5 | 22/25 | Keep |
| Flex Grow | 5 | 5 | 5 | 2 | 5 | 22/25 | Keep |
| Align Self | 5 | 5 | 5 | 2 | 4 | 21/25 | Keep |
**Module Summary:** MAJOR ISSUE - First lesson has minimal explanation compared to CSS Selectors!
**Issues:**
- Q4: **CRITICAL** - Lesson 1 description is only 2 sentences + code block
- Q4: No "why" context about flexbox history or when to use it
- Q4: All lessons have similar minimal depth - no progressive reduction
**Recommendation:**
- **REWRITE lesson 1** with comprehensive explanation:
- History: Before flexbox, layout was done with floats/positioning
- Why: Flexbox solves the "vertical centering problem" and responsive layouts
- Main vs cross axis explained
- When to use flexbox vs grid
- Keep lessons 2-6 brief (appropriate for later lessons)
---
## Module 14: CSS Responsive Design (08-responsive.json)
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| Media Queries | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Fluid Type | 5 | 5 | 5 | 3 | 5 | 23/25 | Keep |
| Responsive Grid | 5 | 4 | 5 | 3 | 5 | 22/25 | Review |
| Mobile-First | 5 | 5 | 5 | 3 | 4 | 22/25 | Keep |
**Module Summary:** Good coverage but has prerequisite issue.
**Issues:**
- Q2: Lesson 3 (Responsive Grid) uses CSS Grid concepts but Grid hasn't been taught!
- GAP: Grid module should come BEFORE Responsive Design
**Recommendation:**
- ADD Grid module before Responsive Design
- Or remove/rewrite Responsive Grid lesson to use flexbox only
---
## Module 15: CSS Animations (06-transitions-animations.json)
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| Transitions | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Timing Funcs | 5 | 5 | 5 | 3 | 5 | 23/25 | Keep |
| Keyframes | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Animation Properties | 5 | 5 | 5 | 3 | 4 | 22/25 | Keep |
**Module Summary:** Good structure with appropriate progression.
**Issues:**
- Q4: First lesson could have more context about when to use animations (UX principles)
**Recommendation:**
- Add brief UX context to lesson 1: "Animations should enhance UX, not distract"
---
## Module 16: Goodbye (99-goodbye.json)
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| Well Done! | 5 | 5 | 5 | 5 | 5 | 25/25 | Keep |
| Contribute | 5 | 5 | 5 | 5 | 5 | 25/25 | Keep |
| Keep Learning | 5 | 5 | 5 | 5 | 5 | 25/25 | Keep |
**Module Summary:** Excellent closing with resources and encouragement.
**Issues:** None
**Recommendation:** Keep as-is
---
# Unused Modules Evaluation
## Colors (03-colors.json) - NOT IN PATH
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| Setting Background Colors | 5 | 5 | 4 | 3 | 5 | 22/25 | Enhance |
| Text Color and Contrast | 5 | 5 | 5 | 3 | 5 | 23/25 | Keep |
| CSS Gradients | 5 | 5 | 5 | 3 | 5 | 23/25 | Keep |
| Background Images | 5 | 5 | 4 | 3 | 4 | 21/25 | Review |
**Issues:**
- Q3: Lesson 1 uses hex code (#e0f7fa) instead of named color - inconsistent with guidelines
- Q4: First lesson needs more explanation about color theory, accessibility
- Q3: Lesson 4 task is vague: "using a placeholder URL"
**Recommendation:**
- **ADD to learning path** after CSS Selectors
- Change hex codes to named colors where possible
- Enhance lesson 1 with color accessibility context (contrast ratios)
- Clarify task in lesson 4
---
## Typography (04-typography.json) - NOT IN PATH
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| Font Family & Fallbacks | 5 | 5 | 5 | 3 | 5 | 23/25 | Enhance |
| Font Size & Line Height | 5 | 5 | 5 | 3 | 5 | 23/25 | Keep |
| Font Weight & Style | 5 | 5 | 5 | 3 | 5 | 23/25 | Keep |
| Text Decoration & Shadow | 5 | 5 | 4 | 3 | 4 | 21/25 | Review |
**Issues:**
- Q4: First lesson needs more context about web fonts, system fonts, why fallbacks matter
- Q3: Lesson 4 task is vague: "Apply an underline... and a light shadow"
**Recommendation:**
- **ADD to learning path** after Colors
- Enhance lesson 1 with font loading context
- Specify exact values in lesson 4 task
---
## Grid (grid.json) - NOT IN PATH
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| Grid Container Basics | 5 | 5 | 5 | 4 | 5 | 24/25 | Enhance |
| Grid Template Areas | 5 | 5 | 5 | 4 | 5 | 24/25 | Keep |
| Spanning Grid Cells | 5 | 5 | 5 | 3 | 5 | 23/25 | Keep |
| Automatic Grid Placement | 5 | 5 | 5 | 3 | 5 | 23/25 | Keep |
| Grid Alignment | 5 | 5 | 5 | 3 | 5 | 23/25 | Keep |
| Overlapping Grid Items | 5 | 5 | 5 | 3 | 4 | 22/25 | Keep |
**Issues:**
- Q4: First lesson needs comprehensive intro about Grid vs Flexbox (when to use which)
**Recommendation:**
- **ADD to learning path** after Flexbox
- Enhance lesson 1 with Grid vs Flexbox comparison
---
## Advanced Selectors (01-advanced-selectors.json) - NOT IN PATH
| Lesson | Q1 | Q2 | Q3 | Q4 | Q5 | Total | Action |
|--------|----|----|----|----|----|----|--------|
| [attribute] | 5 | 5 | 5 | 5 | 5 | 25/25 | Keep |
| Attr Matching | 5 | 5 | 5 | 5 | 5 | 25/25 | Keep |
| Child (>) | 5 | 5 | 5 | 5 | 5 | 25/25 | Keep |
| Descendant | 5 | 5 | 5 | 5 | 5 | 25/25 | Keep |
| Adjacent (+) | 5 | 5 | 5 | 5 | 5 | 25/25 | Keep |
| Sibling (~) | 5 | 5 | 5 | 5 | 5 | 25/25 | Keep |
| :hover | 5 | 5 | 5 | 5 | 5 | 25/25 | Keep |
| :first-child | 5 | 5 | 5 | 5 | 5 | 25/25 | Keep |
**Issues:** None - this is an excellent module!
**Recommendation:**
- **ADD to learning path** after CSS Basic Selectors
- This module has the BEST explanation depth of all modules
---
# Summary
## Overall Statistics
| Category | Count |
|----------|-------|
| Total Active Lessons | 62 |
| Lessons Scoring 24-25 | 35 (56%) |
| Lessons Scoring 21-23 | 22 (35%) |
| Lessons Scoring <21 | 5 (8%) |
| Modules to REMOVE | 1 (Marquee) |
| Modules to ADD | 4 (Colors, Typography, Grid, Adv Selectors) |
## Critical Issues
### 1. Missing Modules (HIGH PRIORITY)
- **Colors** - Students use colors without learning them
- **Typography** - Font properties not taught
- **Grid** - Essential layout missing
- **Advanced Selectors** - Excellent content not being used
### 2. Explanation Depth Issues (MEDIUM PRIORITY)
- **Flexbox Lesson 1** - Needs comprehensive rewrite (currently only 2 sentences)
- **HTML Block & Inline Lesson 1** - Needs "why" context
- **Box Model Lesson 1** - Could be more comprehensive
### 3. Module Position Issues (MEDIUM PRIORITY)
- **SVG** - Should be in CSS section, not HTML section
- **Responsive Design** - Uses Grid concepts before Grid is taught
### 4. Deprecated Content (HIGH PRIORITY)
- **Marquee** - Remove from learning path
## Recommended New Structure
```
1. Welcome
2. HTML Block & Inline
3. HTML Forms - Basics
4. HTML Forms - Validation
5. HTML Details & Summary
6. HTML Progress & Meter
7. HTML Tables
8. CSS Selectors - Basics (existing)
9. CSS Selectors - Advanced (ADD - excellent content)
10. CSS Colors (ADD)
11. CSS Typography (ADD)
12. CSS Box Model
13. CSS Units & Variables
14. CSS SVG (MOVE from HTML)
15. CSS Flexbox (ENHANCE lesson 1)
16. CSS Grid (ADD)
17. CSS Responsive Design
18. CSS Animations
19. What's Next?
REMOVED: HTML Marquee
```
## Priority Actions
### Immediate (Before Next Release)
1. Remove HTML Marquee from `src/config/lessons.js`
2. Remove "div & span" lesson from HTML Block & Inline (redundant)
3. Add Advanced Selectors to path after Basic Selectors
4. Enhance Flexbox lesson 1 with comprehensive explanation
### Short-term
5. Add Colors module to path (after Basic Selectors)
6. Add Typography module to path (after Colors)
7. Add Grid module to path (after Flexbox)
8. Move SVG from HTML section to CSS section
### Medium-term
9. Enhance all first lessons with comprehensive explanations
10. Fix hex colors in Colors module to use named colors
11. Clarify vague tasks (Typography lesson 4, Colors lesson 4)
### Long-term
12. Implement progressive depth reduction across all modules
13. Update translations for new module order
---
## UI Changes
### Header Logo
- **Current:** "code" has background highlight, "crispies" is plain
- **Change:** Swap so "crispies" has background highlight, "code" is plain
---
# Detailed Lesson-Level Improvements
## HTML Section Lessons
### Module: HTML Block & Inline (20-html-elements.json)
#### Lesson 1: Block vs Inline Elements
**Current Description:**
> "HTML elements fall into two main categories: Block elements (containers) start on a new line and take full width. Examples: div, p, h1, section. Inline elements flow within text and only take needed width. Examples: span, a, strong, em"
**Issues:**
- No "why" context - why does HTML distinguish between block and inline?
- No historical context
- No practical application explanation
**Improved Description:**
```
<strong>Understanding Document Flow</strong><br><br>
When browsers render HTML, they follow a concept called <em>document flow</em> - the natural way elements arrange themselves on a page. This fundamental concept, dating back to HTML's origins as a document markup language, divides elements into two categories:<br><br>
<strong>Block elements</strong> behave like paragraphs in a word processor - they start on a new line and stretch to fill available width. Think of them as "containers" that stack vertically. Examples: <kbd>&lt;div&gt;</kbd>, <kbd>&lt;p&gt;</kbd>, <kbd>&lt;h1&gt;</kbd>, <kbd>&lt;section&gt;</kbd><br><br>
<strong>Inline elements</strong> behave like words in a sentence - they flow within text without breaking the line, taking only the width they need. Examples: <kbd>&lt;span&gt;</kbd>, <kbd>&lt;a&gt;</kbd>, <kbd>&lt;strong&gt;</kbd>, <kbd>&lt;em&gt;</kbd><br><br>
<strong>Why this matters:</strong> Understanding block vs inline is essential for CSS layout. You cannot set width/height on inline elements, and vertical margins behave differently. This distinction forms the foundation of all web layout.
```
#### Lesson 2: Semantic Tags
**Current Description:** Good, but could add brief "why" for accessibility
**Add to beginning:**
```
Semantic HTML helps both browsers and assistive technologies (like screen readers) understand your content structure. Using the right semantic elements improves accessibility and SEO.<br><br>
```
#### Lesson 3: div & span
**Action:** REMOVE (redundant - block/inline already covered in lesson 1)
---
### Module: HTML Forms - Basics (21-html-forms-basic.json)
#### Lesson 1: Form Structure
**Current Description:**
> "Every form needs a form wrapper. Inside, use label to describe inputs and input for user data entry. The for attribute on labels should match the id on inputs for accessibility."
**Issues:**
- No context about what forms ARE or why they exist
- No mention of HTTP form submission
**Improved Description:**
```
<strong>HTML forms are the primary way users send data to websites</strong> - from login credentials to search queries to payment information. Without forms, the web would be read-only.<br><br>
Every form needs a <kbd>&lt;form&gt;</kbd> wrapper that defines where data goes when submitted. Inside, you build the interface with:<br><br>
• <kbd>&lt;label&gt;</kbd> - Describes what each input is for<br>
• <kbd>&lt;input&gt;</kbd> - Where users enter data<br><br>
<strong>Accessibility tip:</strong> The <kbd>for</kbd> attribute on labels should match the <kbd>id</kbd> on inputs. This lets users click the label to focus the input - essential for users with motor impairments.
```
#### Lesson 2: Input Types
**Status:** Good as-is (score 24/25)
#### Lesson 3: Submit Button
**Current Description:** Good but could be briefer (this is lesson 3)
**Action:** Reduce slightly - appropriate for late-module position
---
### Module: HTML Forms - Validation (22-html-forms-validation.json)
#### Lesson 3: Full Form (Complete Registration)
**Current Task:**
> "Complete the registration form. Add required attributes, proper input types, and validation constraints."
**Issue:** Task is vague - doesn't specify exactly what to add
**Improved Task:**
```
Complete the registration form by adding:<br>
1. <kbd>required</kbd> to all fields marked with *<br>
2. <kbd>type="email"</kbd> to the email input<br>
3. <kbd>type="password"</kbd> to the password input<br>
4. <kbd>minlength="8"</kbd> to the password input<br>
5. <kbd>required</kbd> to the terms checkbox
```
---
### Module: HTML Details & Summary (23-html-details-summary.json)
#### Lesson 1: First Widget
**Add to description beginning:**
```
Before HTML5, creating collapsible content required JavaScript. The <kbd>&lt;details&gt;</kbd> element gives you this functionality natively - no scripts needed, fully accessible, works everywhere.<br><br>
```
---
### Module: HTML Tables (30-html-tables.json)
#### Lesson 1: Basic Table Structure
**Add to description beginning:**
```
<strong>Important:</strong> Tables are for <em>tabular data</em> only (like spreadsheets, schedules, or comparison charts). Never use tables for page layout - that's what CSS Grid and Flexbox are for.<br><br>
```
---
### Module: HTML SVG (32-html-svg.json)
#### Lesson 1: Drawing Circles
**Add to description beginning:**
```
<strong>SVG (Scalable Vector Graphics)</strong> creates images using mathematical descriptions rather than pixels. Unlike JPG or PNG, SVG images stay crisp at any size - perfect for logos, icons, and illustrations.<br><br>
```
---
## CSS Section Lessons
### Module: CSS Selectors (00-basic-selectors.json)
**Status:** EXCELLENT - Use as template for other modules. No changes needed.
---
### Module: CSS Box Model (01-box-model.json)
#### Lesson 1: Box Model Components
**Current 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."
**Issues:**
- Good but could be more comprehensive like CSS Selectors lesson 1
**Improved Description:**
```
<strong>The CSS box model is the foundation of all web layout.</strong> Every HTML element is rendered as a rectangular box, and understanding this box is crucial for controlling spacing and sizing.<br><br>
The box model consists of four concentric layers (from inside out):<br>
1. <strong>Content</strong> - Your actual text, images, or nested elements<br>
2. <strong>Padding</strong> - Space between content and border (inside the element)<br>
3. <strong>Border</strong> - The visible edge of the element<br>
4. <strong>Margin</strong> - Space between this element and others (outside the element)<br><br>
<pre>.box {
/* Content is determined by width/height */
padding: 20px; /* Inside spacing */
border: 2px solid black;
margin: 10px; /* Outside spacing */
}</pre>
<strong>Key insight:</strong> Padding adds to the element's visual size; margin creates empty space around it.
```
---
### Module: CSS Flexbox (flexbox.json) - CRITICAL
#### Lesson 1: Container
**Current Description:**
> "Learn how to create a flex container and understand the main and cross axes.
> [code block]"
**Issues:**
- ONLY 2 sentences before code block!
- No "why" context
- No comparison to older methods
- No explanation of when to use flexbox
**Improved Description:**
```
<strong>Flexbox revolutionized CSS layout.</strong> Before flexbox (2012), developers used floats and positioning hacks for layouts - techniques that were never designed for that purpose. Flexbox was created specifically to solve common layout problems.<br><br>
<strong>When to use Flexbox:</strong><br>
• Centering content (finally easy!)<br>
• Distributing space between items<br>
• Aligning items in a row or column<br>
• Creating responsive navigation bars<br>
• Building card layouts<br><br>
<strong>The two axes:</strong><br>
Flexbox works along two perpendicular axes:<br>
• <strong>Main axis</strong> - The direction items flow (horizontal by default)<br>
• <strong>Cross axis</strong> - Perpendicular to main axis (vertical by default)<br><br>
<pre>.container {
display: flex; /* Creates flex container */
justify-content: center; /* Align on main axis */
align-items: center; /* Align on cross axis */
}</pre>
To start, you only need <kbd>display: flex</kbd> on the parent container. This immediately makes all direct children "flex items" that can be aligned and distributed.
```
#### Lessons 2-6
**Status:** Good as-is - appropriately brief for later lessons
---
### Module: CSS Responsive Design (08-responsive.json)
#### Lesson 1: Media Queries
**Add to description beginning:**
```
<strong>Responsive design</strong> means your layout adapts to different screen sizes - from phones to desktops. Media queries are CSS rules that only apply when certain conditions (like screen width) are met.<br><br>
```
---
### Module: CSS Animations (06-transitions-animations.json)
#### Lesson 1: Transitions
**Add to description:**
```
<strong>UX principle:</strong> Animations should enhance usability, not distract. Use subtle, fast transitions (0.2-0.3s) for interactive feedback. Reserve longer animations for meaningful state changes.<br><br>
```
---
## Unused Modules to Add
### Module: CSS Colors (03-colors.json)
#### Lesson 1: Setting Background Colors
**Current Issues:**
- Uses hex code (#e0f7fa) instead of named color
- No context about color accessibility
**Improved Description:**
```
<strong>Color is fundamental to web design</strong>, but it's also one of the most important accessibility considerations. Approximately 8% of men have some form of color blindness.<br><br>
CSS offers multiple ways to specify colors:<br>
• <strong>Named colors:</strong> <kbd>steelblue</kbd>, <kbd>coral</kbd>, <kbd>gold</kbd> (147 names)<br>
• <strong>Hex codes:</strong> <kbd>#3498db</kbd> (harder to read)<br>
• <strong>RGB:</strong> <kbd>rgb(52, 152, 219)</kbd><br>
• <strong>HSL:</strong> <kbd>hsl(204, 70%, 53%)</kbd> (easiest to adjust)<br><br>
For learning, we'll use named colors - they're memorable and readable.
```
**Fix task:** Change `#e0f7fa` to `lightcyan` (equivalent named color)
#### Lesson 4: Background Images
**Current Task:** "Set a background image on '.bg-img' using a placeholder URL, center it, and prevent tiling."
**Issue:** Vague - what URL? What values?
**Improved Task:**
```
The background image is already set. Add these properties to <kbd>.bg-img</kbd>:<br>
1. <kbd>background-position: center</kbd><br>
2. <kbd>background-repeat: no-repeat</kbd>
```
---
### Module: CSS Typography (04-typography.json)
#### Lesson 1: Font Family & Fallbacks
**Add context:**
```
<strong>Web fonts vs System fonts:</strong> Web fonts (like Google Fonts) load from the internet, adding download time. System fonts (like Georgia, Arial) are already on users' devices - instant and reliable.<br><br>
<strong>Fallback stacks</strong> ensure text displays even if your preferred font fails to load. Always end with a generic family (<kbd>serif</kbd>, <kbd>sans-serif</kbd>, <kbd>monospace</kbd>).
```
#### Lesson 4: Text Decoration & Shadow
**Current Task:** "Apply an underline with text-decoration and a light shadow using text-shadow on '.fancy'."
**Issue:** Doesn't specify shadow values
**Improved Task:**
```
Style <kbd>.fancy</kbd> with:<br>
1. <kbd>text-decoration: underline</kbd><br>
2. <kbd>text-shadow: 1px 1px 2px gray</kbd>
```
---
### Module: CSS Grid (grid.json)
#### Lesson 1: Grid Container Basics
**Add context about Grid vs Flexbox:**
```
<strong>Grid vs Flexbox - when to use which?</strong><br><br>
• <strong>Flexbox:</strong> One-dimensional layouts (a row OR a column)<br>
• <strong>Grid:</strong> Two-dimensional layouts (rows AND columns together)<br><br>
Use Grid when you need to align items in both directions simultaneously - like a photo gallery, dashboard, or page layout. Use Flexbox for navigation bars, card rows, or centering content.<br><br>
Grid was the last major layout feature added to CSS (2017), completing the layout toolkit that web designers had wanted for decades.
```
---
# Summary: Lessons Requiring Changes
| Module | Lesson | Change Type | Priority |
|--------|--------|-------------|----------|
| HTML Block & Inline | 1. Block vs Inline | Rewrite description | HIGH |
| HTML Block & Inline | 2. Semantic Tags | Add accessibility context | MEDIUM |
| HTML Block & Inline | 3. div & span | **REMOVE** | HIGH |
| HTML Forms | 1. Form Structure | Enhance description | MEDIUM |
| HTML Forms Validation | 3. Full Form | Clarify task | MEDIUM |
| HTML Details | 1. First Widget | Add history context | LOW |
| HTML Tables | 1. Basic Table | Add "not for layout" warning | MEDIUM |
| HTML SVG | 1. Drawing Circles | Add SVG vs raster context | LOW |
| CSS Box Model | 1. Components | Enhance description | MEDIUM |
| **CSS Flexbox** | **1. Container** | **REWRITE description** | **CRITICAL** |
| CSS Responsive | 1. Media Queries | Add responsive context | LOW |
| CSS Animations | 1. Transitions | Add UX principle | LOW |
| CSS Colors | 1. Background Colors | Fix hexnamed, add a11y | MEDIUM |
| CSS Colors | 4. Background Images | Clarify task | MEDIUM |
| CSS Typography | 1. Font Family | Add web font context | MEDIUM |
| CSS Typography | 4. Decoration | Specify shadow values | MEDIUM |
| CSS Grid | 1. Container Basics | Add Grid vs Flexbox | MEDIUM |

24
LICENSE
View File

@@ -1,24 +0,0 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

View File

@@ -1,16 +1,16 @@
![Code Crispies Logo](./public/android-chrome-192x192.png)
# Code Crispies
An interactive platform for learning HTML, CSS, and Tailwind CSS through practical challenges.
An interactive platform for learning CSS and Tailwind CSS through practical challenges.
## 📚 Overview
Code Crispies is a web-based learning platform designed to help users master HTML, CSS, and Tailwind CSS through hands-on exercises. The application presents a series of progressive challenges organized into themed modules, allowing learners to build their skills step by step while receiving immediate feedback.
Code Crispies is a web-based learning platform designed to help users master CSS and Tailwind CSS through hands-on exercises. The application presents a series of progressive challenges organized into themed modules, allowing learners to build their skills step by step while receiving immediate feedback.
### Key Features
- **Interactive Lessons**: Learn HTML, CSS, and Tailwind concepts through practical, hands-on challenges
- **Multiple Learning Paths**: CSS fundamentals, HTML semantics, and Tailwind CSS utilities
- **Interactive Lessons**: Learn CSS and Tailwind concepts through practical, hands-on challenges
- **Dual Mode Support**: Switch between CSS and Tailwind CSS learning modes
- **Progressive Difficulty**: Modules are structured to build skills gradually from basic to advanced
- **Real-Time Feedback**: Get immediate validation on your code solutions with comprehensive feedback
- **Progress Tracking**: Your learning progress is automatically saved in the browser
@@ -256,4 +256,4 @@ When adding new lessons:
## 📄 License
This project is released into the public domain under the [Unlicense](./LICENSE). You are free to copy, modify, publish, use, compile, sell, or distribute this software for any purpose.
Copyright (c) 2026 Michael Czechowski. Licensed under the [./LICENSE](WTFPL).

View File

@@ -1,297 +0,0 @@
# Code Crispies Roadmap
## Current State (Updated)
**Total Active Lessons:** 101 (excluding welcome, goodbye, playground)
**Target:** 100 lessons for milestone system ✅ ACHIEVED
### Current Module Breakdown
| Module | Lessons | Category | Status |
|--------|---------|----------|--------|
| Basic Selectors | 10 | CSS | ✅ |
| Colors | 4 | CSS | ✅ |
| **Gradients** | 3 | CSS | ✅ NEW |
| Typography | 6 | CSS | ✅ +2 |
| Box Model | 8 | CSS | ✅ |
| Flexbox | 6 | CSS | ✅ |
| Grid | 6 | CSS | ✅ |
| **Positioning** | 4 | CSS | ✅ NEW |
| Units & Variables | 4 | CSS | ✅ |
| Responsive | 4 | CSS | ✅ |
| Transitions & Animations | 4 | CSS | ✅ |
| **Filters** | 4 | CSS | ✅ NEW |
| **Pseudo-elements** | 4 | CSS | ✅ NEW |
| HTML Elements | 2 | HTML | ✅ |
| **Semantic HTML** | 3 | HTML | ✅ NEW |
| Figure | 3 | HTML | ✅ |
| SVG | 3 | HTML | ✅ |
| Details/Summary | 3 | HTML | ✅ |
| Dialog | 2 | HTML | ✅ |
| Progress/Meter | 3 | HTML | ✅ |
| Forms Basic | 3 | HTML | ✅ |
| Forms Validation | 1 | HTML | ✅ |
| Fieldset | 3 | HTML | ✅ |
| Datalist | 2 | HTML | ✅ |
| Tables | 3 | HTML | ✅ +2 |
| **Total** | **101** | | ✅ |
---
## Phase 1: Milestone Progress System ✅ COMPLETED
### Design
Replace percentage-based progress with milestone markers:
```
[1] [5] [10] [20] [30] [50] [75] [100]
● ● ◐ ○ ○ ○ ○ ○
```
**Milestones:**
- 1 lesson - First Step
- 5 lessons - Getting Started
- 10 lessons - Rookie
- 20 lessons - Learner
- 30 lessons - Intermediate
- 50 lessons - Halfway Hero
- 75 lessons - Advanced
- 100 lessons - Master
### Implementation ✅
1. **Update `LessonEngine.getProgressStats()`**
- Added `currentMilestone` and `nextMilestone` fields
- Added `milestonesReached: number[]`
- Added `progressToNext` percentage
2. **Update Progress UI**
- Added milestone dots with visual states (reached, current, next)
- Animate milestone completion
- Show current milestone badge
3. **Add Milestone Celebration**
- Confetti/animation on reaching milestones
- Achievement unlocks in sidebar
---
## Phase 2: New Lessons (34 needed to reach 100)
### Priority 1: Expand Existing Modules (+15 lessons)
#### CSS Colors (+3)
- Gradients (linear-gradient)
- Color functions (hsl, rgb)
- Opacity and transparency
#### Typography (+3)
- Web fonts (@font-face)
- Text shadows
- Letter/word spacing
#### Responsive (+3)
- Container queries
- Aspect ratio
- Clamp() for fluid typography
#### Transitions & Animations (+3)
- Keyframe animations
- Animation timing functions
- Transform origin
#### Tables (+3)
- Table styling (borders, spacing)
- Responsive tables
- Table accessibility
### Priority 2: New CSS Modules (+12 lessons)
#### Filters & Effects (4 lessons)
- CSS filters (blur, brightness, contrast)
- Backdrop filters
- Mix-blend-mode
- Box shadows advanced
#### Positioning (4 lessons)
- Relative positioning
- Absolute positioning
- Fixed/sticky positioning
- Z-index stacking
#### Pseudo-elements (4 lessons)
- ::before and ::after
- ::first-letter and ::first-line
- ::marker for lists
- Content property
### Priority 3: New HTML Modules (+7 lessons)
#### Semantic Structure (3 lessons)
- Article vs Section
- Header/Footer/Main
- Nav and Aside
#### Media Elements (2 lessons)
- Picture element (responsive images)
- Audio/Video basics
#### Accessibility (2 lessons)
- ARIA labels
- Skip links
- Focus management
---
## MDN Topics Reference
### CSS Topics from MDN (prioritized for interactive lessons)
**Layout Systems:**
- [x] Flexbox (covered)
- [x] Grid (covered)
- [ ] Multi-column layout
- [ ] Positioned layout (z-index, stacking)
**Visual Effects:**
- [x] Colors (partially covered)
- [ ] Filters (blur, brightness, contrast, drop-shadow)
- [ ] Blend modes (mix-blend-mode, background-blend-mode)
- [ ] Masking and clipping
- [ ] Shapes (shape-outside)
**Typography:**
- [x] Basic text (covered)
- [ ] Web fonts (@font-face)
- [ ] Variable fonts
- [ ] Text decoration advanced
**Animations:**
- [x] Transitions (covered)
- [ ] Keyframe animations
- [ ] Scroll-driven animations (experimental)
- [ ] View transitions
**Advanced:**
- [x] Custom properties (covered in units-variables)
- [ ] Container queries
- [ ] Anchor positioning (new)
- [ ] Logical properties (for RTL support)
### HTML Topics from MDN
**Structural:**
- [x] Basic elements (covered)
- [x] Figure/figcaption (covered)
- [ ] Article vs section semantics
- [ ] Template element
**Interactive:**
- [x] Details/Summary (covered)
- [x] Dialog (have JSON, not active)
- [ ] Datalist (have JSON, not active)
- [ ] Progress/Meter (have JSON, not active)
**Forms:**
- [x] Basic forms (covered)
- [x] Validation (covered)
- [x] Fieldset (have JSON, not active)
- [ ] Input types exploration
**Media:**
- [x] SVG basics (covered)
- [ ] Picture element
- [ ] srcset and sizes
- [ ] Audio/Video
---
## Inactive Lesson Files (Ready to Activate)
These lesson files exist but aren't in the active module list:
| File | Lessons | Topic |
|------|---------|-------|
| 24-html-progress-meter.json | 3 | Progress/Meter elements |
| 25-html-datalist.json | 2 | Datalist for autocomplete |
| 27-html-dialog.json | 2 | Native dialog element |
| 28-html-forms-fieldset.json | 3 | Fieldset/legend grouping |
| 31-html-marquee.json | 3 | Marquee (deprecated but fun) |
| **Total** | **13** | |
**Quick Win:** Activating these adds 13 lessons immediately → 79 total
---
## Implementation Order
### Week 1: Foundation
1. Design milestone UI component
2. Implement milestone progress system
3. Add milestone celebrations
### Week 2: Quick Wins
4. Activate 5 inactive HTML modules (+13 lessons)
5. Test and fix translations
### Week 3-4: New Content
6. Create Filters & Effects module (+4)
7. Create Positioning module (+4)
8. Expand existing modules (+7)
### Final Polish
9. Reach 100 lessons milestone
10. Add milestone achievements to sidebar
11. Update landing page messaging
---
## Technical Notes
### Milestone Data Structure
```js
const MILESTONES = [1, 5, 10, 20, 30, 50, 75, 100];
function getMilestoneProgress(completed) {
const reached = MILESTONES.filter(m => completed >= m);
const current = reached[reached.length - 1] || 0;
const next = MILESTONES.find(m => m > completed) || 100;
return {
current,
next,
reached,
percentToNext: ((completed - current) / (next - current)) * 100
};
}
```
### Progress Display Update
```html
<div class="milestone-progress">
<div class="milestones">
<span class="milestone reached" data-value="1">1</span>
<span class="milestone reached" data-value="5">5</span>
<span class="milestone current" data-value="10">10</span>
<span class="milestone" data-value="20">20</span>
<!-- ... -->
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 35%"></div>
</div>
<span class="progress-label">12 of 100 lessons</span>
</div>
```
---
## Success Metrics
- [ ] 100 total lessons
- [ ] Milestone system implemented
- [ ] All 6 languages have translations
- [ ] Achievement celebrations working
- [ ] Mobile responsive milestone UI

61
flake.lock generated
View File

@@ -1,61 +0,0 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1768564909,
"narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -9,14 +9,13 @@
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; config.allowUnfree = true; };
pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_20
nodePackages.npm
gnumake
claude-code
];
shellHook = ''

View File

@@ -1,257 +1,588 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "css-basic-selectors",
"title": "CSS Basics",
"description": "Learn the fundamental building blocks of CSS: properties, values, and selectors. This module teaches you the syntax rules that every CSS declaration follows.",
"title": "CSS Selectors",
"description": "CSS selectors are the foundation of styling web pages, allowing you to target specific HTML elements for styling. This module introduces fundamental selector types including element type selectors, class selectors, ID selectors, and the universal selector.",
"difficulty": "beginner",
"lessons": [
{
"id": "css-properties",
"title": "CSS Properties",
"description": "CSS styles elements using <strong>declarations</strong> - pairs of properties and values. Every declaration follows the same pattern:<br><br><pre>property: value;</pre><br>The <strong>property</strong> is what you want to change (like <kbd>color</kbd> or <kbd>background</kbd>). The <strong>value</strong> is what you're setting it to. A colon separates them, and a semicolon ends the line.<br><br>Values come in different types:<br>• <strong>Keywords:</strong> <kbd>red</kbd>, <kbd>bold</kbd>, <kbd>center</kbd><br>• <strong>Numbers with units:</strong> <kbd>16px</kbd>, <kbd>2rem</kbd>, <kbd>100%</kbd><br>• <strong>Colors:</strong> <kbd>steelblue</kbd>, <kbd>#ff0000</kbd>",
"task": "Complete the declaration by adding <kbd>color: coral;</kbd> to change the text color.",
"previewHTML": "<p class=\"text\">This text should turn coral.</p>",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } .text { font-size: 1.25rem; }",
"sandboxCSS": "",
"codePrefix": ".text {\n ",
"id": "introduction-to-selectors",
"title": "What's a Selector?",
"description": "A CSS selector is the first part of a CSS rule that tells the browser which HTML elements should receive the styles defined in the declaration block. Selectors are essentially patterns that match against elements in your HTML document. Understanding selectors is fundamental because they determine which elements your CSS rules will affect. The element or elements targeted by a selector are referred to as the 'subject of the selector.' When writing a CSS rule, you first specify the selector, followed by curly braces that contain the style declarations.<br/>For example, to change the text color of elements, you can use the <kbd>color</kbd> property within your declaration block.<br><br><pre>/* Element selector */\np {\n color: orangered;\n /* │ └─── Indicates the value of the expression\n │ \n └─────────── Indicates the property of the expression */\n}</pre>",
"task": "Write a CSS rule using a type selector that targets all paragraph elements <kbd>p</kbd> in the document. Make the text blue by setting the <kbd>color</kbd> property to <kbd>blue</kbd>.",
"previewHTML": "<h1>Introduction to CSS Selectors</h1>\n<p>This paragraph should turn blue.</p>\n<div>This div element should remain unchanged.</div>\n<p>This second paragraph should also turn blue.</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h1, p, div { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
"codePrefix": "/* Write a type selector to target all paragraph elements */\n",
"initialCode": "",
"codeSuffix": "\n}",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "color: coral;",
"solution": "p { color: blue }",
"concept": {
"explanation": "Selectors are pattern-matching rules that tell the browser which HTML elements to style. The browser scans through your HTML document's DOM tree, testing each element against your selector pattern. When an element matches, the browser applies the styles. This is why the p selector affects both paragraphs but not the h1 or div—only elements with the tag name 'p' match the pattern.",
"diagram": "HTML Document (DOM Tree)\n\n<body>\n <h1>Title</h1> ← p selector: NO MATCH\n <p>Text</p> ← p selector: MATCH ✓\n <div>Box</div> ← p selector: NO MATCH\n <p>More</p> ← p selector: MATCH ✓\n</body>\n\nResult: Only <p> elements get styled"
},
"validations": [
{
"type": "property_value",
"value": { "property": "color", "expected": "coral" },
"message": "Add <kbd>color: coral;</kbd>"
}
]
},
{
"id": "multiple-properties",
"title": "Multiple Properties",
"description": "A rule can have multiple declarations. Each one goes on its own line, and each needs a semicolon at the end:<br><br><pre>.box {<br> background: gold;<br> color: navy;<br> padding: 1rem;<br>}</pre><br>The order usually doesn't matter - CSS applies them all. When properties conflict, the last one wins.",
"task": "Add two declarations: <kbd>background: lavender;</kbd> and <kbd>padding: 1rem;</kbd>",
"previewHTML": "<div class=\"card\">A styled card with background and padding.</div>",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } .card { border-radius: 8px; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"previewContainer": "preview-area",
"solution": "background: lavender;\n padding: 1rem;",
"validations": [
"type": "regex",
"value": "^p\\s*{",
"message": "Start your rule with <kbd>p { … }</kbd> to select all paragraph elements",
"options": {
"caseSensitive": false
}
},
{
"type": "property_value",
"value": { "property": "background", "expected": "lavender" },
"message": "Add <kbd>background: lavender;</kbd>"
"type": "contains",
"value": "color:",
"message": "Include the <kbd>color:</kbd> property in your CSS rule"
},
{
"type": "contains",
"value": "blue",
"message": "Set the color value to <kbd>blue</kbd>"
},
{
"type": "property_value",
"value": { "property": "padding", "expected": "1rem" },
"message": "Add <kbd>padding: 1rem;</kbd>"
"value": {
"property": "color",
"expected": "blue"
},
"message": "Use <kbd>color: blue</kbd> to set the text color"
},
{
"type": "regex",
"value": "p\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": false
}
}
]
},
{
"id": "type-selectors",
"title": "Type Selectors",
"description": "A <strong>selector</strong> tells the browser which elements to style. The simplest selector is a <strong>type selector</strong> — just the HTML tag name.<br><br><pre>p {<br> color: steelblue;<br>}</pre><br>This rule targets every <kbd>&lt;p&gt;</kbd> element on the page. Type selectors are great for setting base styles.",
"task": "Style all paragraphs. Write a rule with <kbd>p</kbd> as the selector and set <kbd>color: steelblue</kbd>.",
"previewHTML": "<article>\n <h2>Fresh Roasted Coffee</h2>\n <p>Our beans are sourced from small farms in Colombia and Ethiopia.</p>\n <p>Each batch is roasted weekly to ensure peak freshness.</p>\n</article>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } h2 { margin: 0 0 1rem; }",
"sandboxCSS": "",
"codePrefix": "",
"initialCode": "",
"description": "Type selectors (also called tag name selectors or element selectors) target HTML elements based on their tag name. For example, <kbd>p</kbd> selects all paragraph elements, <kbd>h1</kbd> selects all level-one headings, and <kbd>div</kbd> selects all division elements. Type selectors are the most fundamental way to select elements, applying styles consistently to all instances of a particular HTML element throughout your document. You can define a variety of CSS properties with type selectors, such as <kbd>color</kbd> for text color, <kbd>background-color</kbd> for the background, and <kbd>font-weight</kbd> for text emphasis. They provide a broad approach for styling your page and are often the starting point for more specific styling using other selector types.",
"task": "Write three separate CSS rules using type selectors to target specific HTML elements: make <kbd>h2</kbd> headings <kbd>purple</kbd>, give <kbd>span</kbd> elements a <kbd>yellow</kbd> background, and make <kbd>strong</kbd> elements <kbd>red</kbd>.",
"previewHTML": "<h2>Type Selectors Example</h2>\n<p>Regular paragraph text <span>with a highlighted span</span> that should have a yellow background.</p>\n<p>Another paragraph with <strong>strong important text</strong> that should be red.</p>\n<h2>Another Heading</h2>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h2, p, span, strong { padding: 3px; }",
"codePrefix": "/* Write three separate type selectors below */\n\n",
"initialCode": "/* 1. Make h2 headings purple */\n\n\n/* 2. Give span elements a yellow background */\n\n\n/* 3. Make strong elements red */\n",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "p {\n color: steelblue;\n}",
"solution": "/* 1. Make h2 headings purple */\nh2 {\n color: purple;\n}\n\n/* 2. Give span elements a yellow background */\nspan {\n background-color: yellow;\n}\n\n/* 3. Make strong elements red */\nstrong {\n color: red;\n}",
"concept": {
"explanation": "Type selectors have the lowest specificity in CSS, which makes them perfect for establishing baseline styles. They cast a wide net—every element of that type gets styled. This is intentional: you set foundational styles with type selectors, then use more specific selectors (classes, IDs) to override individual elements when needed.",
"diagram": "Type Selector Specificity\n\nLow specificity = applies broadly\n\nh2 { color: purple; }\n ↓\nMatches ALL <h2> elements\n ↓\n<h2>First</h2> ✓ purple\n<h2>Second</h2> ✓ purple\n<h2>Third</h2> ✓ purple"
},
"validations": [
{
"type": "regex",
"value": "p\\s*\\{",
"message": "Start with <kbd>p {</kbd> to select paragraphs"
"value": "^h2\\s*{",
"message": "Include an <kbd>h2 { … }</kbd> selector"
},
{
"type": "property_value",
"value": { "property": "color", "expected": "steelblue" },
"message": "Set <kbd>color: steelblue</kbd>"
}
]
},
{
"id": "styling-links",
"title": "Styling Links",
"description": "Type selectors work for any HTML element. The <kbd>a</kbd> selector targets all links on a page.<br><br>Links have a default blue color and underline. You can change both with CSS — use <kbd>color</kbd> for the text and <kbd>text-decoration: none</kbd> to remove the underline.",
"task": "Style the navigation links. Write a rule with <kbd>a</kbd> as the selector and set <kbd>color: coral</kbd>.",
"previewHTML": "<nav>\n <a href=\"#\">Home</a>\n <a href=\"#\">Menu</a>\n <a href=\"#\">About</a>\n <a href=\"#\">Contact</a>\n</nav>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } nav { display: flex; gap: 1.5rem; }",
"sandboxCSS": "",
"codePrefix": "",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "a {\n color: coral;\n}",
"validations": [
"value": {
"property": "color",
"expected": "purple"
},
"message": "Set the <kbd>color</kbd> property to <kbd>purple</kbd> for h2 elements"
},
{
"type": "regex",
"value": "a\\s*\\{",
"message": "Start with <kbd>a {</kbd> to select links"
"value": "h2\\s*{[^}]*}",
"message": "Make sure to close your h2 rule with a closing brace <kbd>}</kbd>"
},
{
"type": "regex",
"value": "^span\\s*{",
"message": "Include a <kbd>span { … }</kbd> selector"
},
{
"type": "property_value",
"value": { "property": "color", "expected": "coral" },
"message": "Set <kbd>color: coral</kbd>"
"value": {
"property": "background-color",
"expected": "yellow"
},
"message": "Set a <kbd>background-color: yellow</kbd> for span elements"
},
{
"type": "regex",
"value": "span\\s*{[^}]*}",
"message": "Make sure to close your span rule with a closing brace <kbd>}</kbd>"
},
{
"type": "regex",
"value": "^strong\\s*{",
"message": "Include a <kbd>strong { … }</kbd> selector"
},
{
"type": "regex",
"value": "strong\\s*{\\s*color:\\s*red;[^}]*}",
"message": "Set the <kbd>color: red</kbd> for strong elements"
}
]
},
{
"id": "class-selectors",
"title": "Class Selectors",
"description": "Type selectors style <em>all</em> elements of that type. But what if you want to style just some of them?<br><br><strong>Class selectors</strong> target elements with a specific <kbd>class</kbd> attribute. They start with a dot:<br><br><pre>.badge {<br> background: coral;<br>}</pre><br>This styles only elements with <kbd>class=\"badge\"</kbd>.",
"task": "Style the notification badge. Write a rule with <kbd>.badge</kbd> as the selector and set <kbd>background: tomato</kbd>.",
"previewHTML": "<header>\n <h1>Dashboard</h1>\n <span class=\"badge\">3</span>\n</header>\n<p>You have new notifications waiting.</p>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem; } h1 { margin: 0; font-size: 1.5rem; } .badge { color: white; padding: 0.25rem 0.5rem; border-radius: 999px; font-size: 0.875rem; } p { color: #555; margin: 0; }",
"sandboxCSS": "",
"codePrefix": "",
"description": "Class selectors target elements with a specific class attribute value. They begin with a dot (.) followed by the class name. Classes are powerful because they allow you to apply the same styles to multiple elements regardless of their type. An HTML element can have multiple classes (separated by spaces in the class attribute), and a class can be applied to any number of elements. When using class selectors, you can apply properties like <kbd>background-color</kbd> to set the background color of elements, and <kbd>font-weight</kbd> to control text thickness, making text bold or normal. This flexibility makes class selectors one of the most commonly used methods for applying styles in CSS, allowing for modular and reusable styling across your website.",
"task": "Create a CSS rule using a class selector that targets elements with the class <kbd>highlight</kbd>. Give these elements a <kbd>yellow</kbd> background and <kbd>bold</kbd> text.",
"previewHTML": "<h2>Using Class Selectors</h2>\n<p>This is a regular paragraph, but <span class=\"highlight\">this span has the highlight class</span> applied to it.</p>\n<p class=\"highlight\">This entire paragraph has the highlight class.</p>\n<ul>\n <li>Regular list item</li>\n <li class=\"highlight\">This list item is highlighted</li>\n</ul>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h2, p, li { padding: 5px; margin-bottom: 10px; }",
"codePrefix": "/* Create a class selector for elements with the 'highlight' class */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": ".badge {\n background: tomato;\n}",
"concept": {
"explanation": "Class selectors match elements by their class attribute, not their tag name. This is powerful because it lets you apply the same styles across different element types—span, p, and li can all share the highlight class. Class selectors have medium specificity, higher than type selectors, so they can override type selector rules.",
"diagram": "Class Selector Matches Attribute\n\n.highlight { ... }\n ↓\nSearches for class=\"highlight\"\n ↓\n<span class=\"highlight\"> ✓ MATCH\n<p class=\"highlight\"> ✓ MATCH\n<li class=\"highlight\"> ✓ MATCH\n<p class=\"other\"> ✗ no match"
},
"validations": [
{
"type": "regex",
"value": "\\.badge\\s*\\{",
"message": "Start with <kbd>.badge {</kbd> (don't forget the dot!)"
"value": "^\\.highlight\\s*{",
"message": "Start your rule with <kbd>.highlight { … }</kbd> to create a class selector",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "background-color:",
"message": "Include the <kbd>background-color:</kbd> property"
},
{
"type": "property_value",
"value": { "property": "background", "expected": "tomato" },
"message": "Set <kbd>background: tomato</kbd>"
"value": {
"property": "background-color",
"expected": "yellow"
},
"message": "Set the background color to <kbd>yellow</kbd>"
},
{
"type": "contains",
"value": "font-weight:",
"message": "Include the <kbd>font-weight:</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "font-weight",
"expected": "bold"
},
"message": "Set the font-weight to <kbd>bold</kbd>"
},
{
"type": "regex",
"value": "\\.highlight\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "button-variants",
"title": "Button Variants",
"description": "Elements can have multiple classes. When you chain class selectors without spaces, you target elements that have <em>all</em> those classes:<br><br><pre>.btn.primary {<br> background: steelblue;<br>}</pre><br>This targets elements with both <kbd>class=\"btn primary\"</kbd>, not just <kbd>.btn</kbd> or just <kbd>.primary</kbd>.",
"task": "Style the primary button. Write a rule with <kbd>.btn.primary</kbd> as the selector and set <kbd>background: steelblue</kbd>.",
"previewHTML": "<div class=\"actions\">\n <button class=\"btn\">Cancel</button>\n <button class=\"btn primary\">Save Changes</button>\n</div>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } .actions { display: flex; gap: 0.75rem; } .btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 1rem; cursor: pointer; background: #e0e0e0; color: #333; }",
"id": "multiple-classes",
"title": "Multiple Classes",
"description": "HTML elements can have multiple classes applied simultaneously, allowing for composable and modular CSS designs. When an element has multiple classes, it will receive styles from all matching class selectors. This approach enables you to build a library of reusable CSS classes that can be combined in different ways. You can also target elements that have a specific combination of classes by chaining class selectors together without spaces (e.g., <kbd>.class1.class2</kbd>). When styling these elements, you might use properties like <kbd>border-color</kbd> to change the color of element borders, and <kbd>background-color</kbd> to set the background color of elements. This technique lets you create conditional styles that only apply when certain classes appear together.",
"task": "Complete the CSS rule that targets elements with both <kbd>card</kbd> and <kbd>featured</kbd> classes by chaining the selectors. Set the border-color to gold and the background-color to lemonchiffon to make featured cards stand out.",
"previewHTML": "<h2>Multiple Class Combinations</h2>\n<div class=\"card\">Regular Card</div>\n<div class=\"card featured\">Featured Card</div>\n<div class=\"featured\">Just Featured (not a card)</div>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .card { border: 2px solid gray; padding: 15px; margin-bottom: 10px; border-radius: 5px; }",
"sandboxCSS": "",
"codePrefix": "",
"codePrefix": "/* The .card class already has basic styling */\n/* Now target elements with BOTH classes: 'card' AND 'featured' */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": ".btn.primary {\n background: steelblue;\n}",
"solution": ".card.featured { border-color: gold; background-color: lemonchiffon; }",
"concept": {
"explanation": "Chaining class selectors with no space between them creates an AND condition—the element must have ALL the classes to match. The selector .card.featured only matches elements with both card and featured in their class attribute. This has higher specificity than a single class, so it can override .card or .featured rules. No space between selectors is crucial—a space would mean descendant relationship instead.",
"diagram": "Chained Selectors = AND Logic\n\n.card.featured { ... }\n ↑ no space = BOTH required\n\n<div class=\"card\"> ✗ missing 'featured'\n<div class=\"card featured\"> ✓ has BOTH\n<div class=\"featured\"> ✗ missing 'card'\n\nSpecificity: 2 classes > 1 class"
},
"validations": [
{
"type": "regex",
"value": "\\.btn\\.primary\\s*\\{",
"message": "Use <kbd>.btn.primary {</kbd> (no space between classes)"
"value": "^\\.card\\.featured\\s*{",
"message": "Chain the selectors as <kbd>.card.featured</kbd> (no space between them)",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "border-color:",
"message": "Include the <kbd>border-color</kbd> property"
},
{
"type": "property_value",
"value": { "property": "background", "expected": "steelblue" },
"message": "Set <kbd>background: steelblue</kbd>"
"value": {
"property": "border-color",
"expected": "gold"
},
"message": "Set the border color to <kbd>gold</kbd>"
},
{
"type": "regex",
"value": "\\.card\\.featured\\s*{[^}]*;",
"message": "Make sure to end your CSS rule with a semicolon <kbd>;</kbd>"
},
{
"type": "contains",
"value": "background-color:",
"message": "Include the <kbd>background-color</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "background-color",
"expected": "lemonchiffon"
},
"message": "Set the background color to <kbd>lemonchiffon</kbd>"
},
{
"type": "regex",
"value": "\\.card\\.featured\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "specific-elements",
"title": "Targeting Specific Elements",
"description": "Sometimes you want a class to look different on different elements. Combine a type selector with a class selector (no space) to be more specific:<br><br><pre>a.btn {<br> text-decoration: none;<br>}</pre><br>This styles only <kbd>&lt;a&gt;</kbd> elements with the <kbd>btn</kbd> class, not <kbd>&lt;button&gt;</kbd> elements with that class.",
"task": "Remove the underline from link buttons. Write a rule with <kbd>a.btn</kbd> as the selector and set <kbd>text-decoration: none</kbd>.",
"previewHTML": "<div class=\"actions\">\n <button class=\"btn\">Regular Button</button>\n <a href=\"#\" class=\"btn\">Link Button</a>\n</div>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } .actions { display: flex; gap: 0.75rem; align-items: center; } .btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 1rem; cursor: pointer; background: steelblue; color: white; }",
"sandboxCSS": "",
"codePrefix": "",
"id": "class-with-type",
"title": "Combining Types",
"description": "You can combine type selectors with class selectors to target specific HTML elements that have a certain class. This creates a more specific selector that only matches when both conditions are true: the element is of the specified type AND it has the specified class. For example, <kbd>p.note</kbd> would select paragraph elements with the class <kbd>note</kbd>, but would not select divs or spans with that same class. You can style these combined selections using properties like <kbd>background-color</kbd> to set a colored background for your elements. This approach allows you to apply different styles to the same class when it appears on different element types.",
"task": "Create a CSS rule that specifically targets <kbd>&lt;span&gt;</kbd> elements with the class <kbd>highlight</kbd>. Make those elements have an orange background, while other elements with the highlight class remain untouched.",
"previewHTML": "<h2>Type and Class Combinations</h2>\n<p>This paragraph has a <span class=\"highlight\">highlighted span</span> that should have an orange background.</p>\n<p class=\"highlight\">This paragraph has the highlight class but should NOT have an orange background.</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .highlight { font-weight: bold; }",
"sandboxCSS": "h2, p, span { padding: 5px; }",
"codePrefix": "/* The .highlight class already sets font-weight to bold */\n/* Now target ONLY span elements with the highlight class */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "a.btn {\n text-decoration: none;\n}",
"concept": {
"explanation": "Combining type and class selectors creates a more specific pattern that requires BOTH conditions. The selector span.highlight only matches span elements with the highlight class—not paragraphs with that class, not spans without it. This higher specificity lets you apply different styles to the same class name depending on which element type it's on, creating contextual variations.",
"diagram": "Type + Class Combination\n\nspan.highlight { ... }\n ↓\nMust be <span> AND have class=\"highlight\"\n ↓\n<span class=\"highlight\"> ✓ MATCH\n<p class=\"highlight\"> ✗ wrong type\n<span class=\"other\"> ✗ wrong class\n\nSpecificity: type + class > class alone"
},
"validations": [
{
"type": "regex",
"value": "a\\.btn\\s*\\{",
"message": "Use <kbd>a.btn {</kbd> (type + class, no space)"
"value": "^span\\.highlight\\s*{",
"message": "Use <kbd>span.highlight</kbd> selector (no space between element and class)",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "background-color:",
"message": "Include the <kbd>background-color</kbd> property"
},
{
"type": "property_value",
"value": { "property": "text-decoration", "expected": "none" },
"message": "Set <kbd>text-decoration: none</kbd>"
"value": {
"property": "background-color",
"expected": "orange"
},
"message": "Set the background color to <kbd>orange</kbd>"
},
{
"type": "regex",
"value": "span\\.highlight\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "grouping-selectors",
"title": "Grouping Selectors",
"description": "When multiple elements need the same styles, list them separated by commas. This keeps your CSS clean and maintainable.<br><br><pre>h1, h2, h3 {<br> color: steelblue;<br>}</pre><br>This applies the same color to all three heading levels in one rule.",
"task": "Style all headings consistently. Add <kbd>color: steelblue</kbd> to the grouped <kbd>h1, h2, h3</kbd> selector.",
"previewHTML": "<article><h1>Main Title</h1><p>Introduction paragraph with some text.</p><h2>Section Heading</h2><p>More content here.</p><h3>Subsection</h3><p>Final paragraph.</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } article { max-width: 500px; } p { color: #555; line-height: 1.6; }",
"sandboxCSS": "",
"codePrefix": "h1, h2, h3 {\n ",
"id": "id-selectors",
"title": "ID Selectors",
"description": "ID selectors target elements with a specific id attribute. They begin with a hash/pound sign (#) followed by the ID name. Unlike classes, IDs must be unique within a document—each ID value should be used only once per page. ID selectors have higher specificity than class or element selectors, meaning they override those selectors when conflicts arise. When styling with ID selectors, you can use properties like <kbd>color</kbd> to define text color, and <kbd>text-decoration</kbd> to control the appearance of text, such as adding underlines to elements. Because of their uniqueness requirement, IDs are best used for one-of-a-kind elements like page headers, main navigation, or specific unique components that appear only once on a page.",
"task": "Create a CSS rule with an ID selector that targets the element with the ID <kbd>main-title</kbd>. Set its color to purple and add an underline with <kbd>text-decoration: underline</kbd>.",
"previewHTML": "<h1 id=\"main-title\">Main Page Title</h1>\n<p>Regular paragraph content.</p>\n<h2>Secondary Heading</h2>\n<p id=\"intro\">Introduction paragraph (different ID).</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h1, h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
"codePrefix": "/* Create an ID selector to target the element with id=\"main-title\" */\n",
"initialCode": "",
"codeSuffix": "\n}",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "color: steelblue;",
"concept": {
"explanation": "ID selectors match a single element with a specific id attribute. Because IDs must be unique per page, #main-title will only ever match one element. IDs have very high specificity—higher than classes—so they override class and type selector rules. This makes IDs powerful but also harder to override later, which is why many developers prefer classes for reusable styles.",
"diagram": "ID Selector High Specificity\n\n#main-title { color: purple; }\n ↓\nMatches ONE element with id=\"main-title\"\n ↓\n<h1 id=\"main-title\"> ✓ MATCH (only one!)\n<h1 id=\"other\"> ✗ different ID\n\nSpecificity Hierarchy:\nID > class > type"
},
"validations": [
{
"type": "regex",
"value": "^#main-title\\s*{",
"message": "Start your rule with <kbd>#main-title</kbd> to create an ID selector",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "color:",
"message": "Include the <kbd>color</kbd> property"
},
{
"type": "property_value",
"value": { "property": "color", "expected": "steelblue" },
"message": "Set <kbd>color: steelblue</kbd>"
"value": {
"property": "color",
"expected": "purple"
},
"message": "Set the color to <kbd>purple</kbd>"
},
{
"type": "contains",
"value": "text-decoration:",
"message": "Include the <kbd>text-decoration</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "text-decoration",
"expected": "underline"
},
"message": "Set the text-decoration to <kbd>underline</kbd>"
},
{
"type": "regex",
"value": "#main-title\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "descendant-selectors",
"title": "Descendant Selectors",
"description": "Target elements inside other elements using a space between selectors. This is one of the most useful patterns in CSS.<br><br><pre>.nav a {<br> color: white;<br>}</pre><br>This styles only links inside <kbd>.nav</kbd>, leaving other links unchanged.",
"task": "Style navigation links differently. Write a rule with <kbd>.nav a</kbd> as the selector and set <kbd>color: white</kbd>.",
"previewHTML": "<nav class=\"nav\"><a href=\"#\">Home</a><a href=\"#\">About</a><a href=\"#\">Contact</a></nav><p>Read more in our <a href=\"#\">documentation</a>.</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; margin: 0; } .nav { background: steelblue; padding: 1rem; display: flex; gap: 1rem; border-radius: 8px; margin-bottom: 1rem; } .nav a { text-decoration: none; } p a { color: steelblue; }",
"sandboxCSS": "",
"codePrefix": "",
"id": "id-with-type",
"title": "Type + ID",
"description": "Similar to how you can combine type and class selectors, you can also combine type selectors with ID selectors. For example, <kbd>h1#title</kbd> targets an h1 element with the ID 'title'. When using this combined approach, you can apply CSS properties like <kbd>font-style</kbd> to control the slant of the text, making it italic or normal. While this selector combination is more specific than using just the ID selector, it's often unnecessary since IDs should already be unique in a document. However, this technique can be useful for improving code readability or when you want to emphasize that a particular ID should only appear on a specific element type.",
"task": "Create a CSS rule that combines a type selector with an ID selector to target specifically a paragraph element with the ID <kbd>special</kbd>. Set its font style to italic.",
"previewHTML": "<h2 id=\"special\">Heading with ID \"special\" (should NOT be affected)</h2>\n<p id=\"special\">Paragraph with ID \"special\" (should become italic)</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
"codePrefix": "/* Create a combined type+ID selector for a paragraph with id=\"special\" */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": ".nav a {\n color: white;\n}",
"concept": {
"explanation": "Combining type and ID adds extra specificity and enforces a pattern—the ID must be on a specific element type. In this example, p#special has even higher specificity than #special alone. This prevents the h2 with the same ID from matching, even though IDs should be unique. This technique is useful when you want to ensure an ID only matches if it's on the correct element type.",
"diagram": "Type + ID Specificity Boost\n\np#special { ... }\n ↓\nMust match BOTH conditions:\n 1. Element type = <p>\n 2. id = \"special\"\n ↓\n<h2 id=\"special\"> ✗ wrong type (not <p>)\n<p id=\"special\"> ✓ MATCH (both pass)\n\nSpecificity: type + ID > ID alone"
},
"validations": [
{
"type": "regex",
"value": "\\.nav\\s+a\\s*\\{",
"message": "Use <kbd>.nav a {</kbd> (space between .nav and a)"
"value": "^p#special\\s*{",
"message": "Use <kbd>p#special</kbd> to target paragraphs with ID <kbd>special</kbd>",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "font-style:",
"message": "Include the <kbd>font-style</kbd> property"
},
{
"type": "property_value",
"value": { "property": "color", "expected": "white" },
"message": "Set <kbd>color: white</kbd>"
"value": {
"property": "font-style",
"expected": "italic"
},
"message": "Set the font-style to <kbd>italic</kbd>"
},
{
"type": "regex",
"value": "p#special\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "nested-styling",
"title": "Nested Styling",
"description": "Descendant selectors let you create contextual styles. The same element can look different depending on where it appears.<br><br>For example, paragraphs in a <kbd>.card</kbd> might be smaller than paragraphs in an <kbd>article</kbd>.",
"task": "Make paragraphs inside the card smaller. Write a rule with <kbd>.card p</kbd> as the selector and set <kbd>font-size: 0.9rem</kbd>.",
"previewHTML": "<article><h2>Article Title</h2><p>This is a regular article paragraph with normal-sized text for comfortable reading.</p><div class=\"card\"><strong>Quick Tip</strong><p>Card paragraphs should be slightly smaller to fit the compact design.</p></div><p>Back to regular article text here.</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } article { max-width: 500px; } h2 { color: steelblue; margin-top: 0; } p { line-height: 1.6; color: #444; } .card { background: #f0f4f8; padding: 1rem; border-radius: 8px; border-left: 4px solid steelblue; } .card strong { color: steelblue; display: block; margin-bottom: 0.5rem; }",
"sandboxCSS": "",
"codePrefix": "",
"id": "selector-lists",
"title": "Selector Lists",
"description": "When multiple elements need the same styling, you can group them together using a selector list (also known as grouping selectors). Selector lists are created by separating individual selectors with commas. This approach reduces repetition in your CSS, making it more maintainable and efficient. For example, <kbd>h1, h2, h3 { color: blue; }</kbd> applies the same blue color to all three heading levels. When styling multiple selectors at once, you can apply properties like <kbd>background-color</kbd> to set the background, <kbd>border-left</kbd> to create a left border with a specific thickness, style, and color, and <kbd>padding-left</kbd> to create space between the content and the left border. Whitespace around commas is optional, and each selector in the list can be any valid selector type-elements, classes, IDs, or even more complex selectors.",
"task": "Create a selector list that applies the same styles to three different elements: paragraphs with class <kbd>note</kbd>, list items with class <kbd>important</kbd>, and the element with ID <kbd>summary</kbd>. Give them a <kbd>lightyellow</kbd> background, a <kbd>gold</kbd> left border, and some left <kbd>padding</kbd>.",
"previewHTML": "<p class=\"note\">This is a note paragraph.</p>\n<ul>\n <li>Regular list item</li>\n <li class=\"important\">Important list item</li>\n</ul>\n<div id=\"summary\">Summary section</div>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "p, li, div { padding: 8px; margin-bottom: 8px; border: 1px dashed gray; }",
"codePrefix": "/* Create a selector list to apply the same styles to multiple different elements */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": ".card p {\n font-size: 0.9rem;\n}",
"solution": "p.note,\nli.important,\n#summary {\n background-color: lightyellow;\n border-left: 3px solid gold;\n padding-left: 10px;\n}",
"concept": {
"explanation": "Selector lists are a shorthand that prevents writing the same properties multiple times. The browser treats each selector in the list independently—it matches elements against each selector separately, then applies the shared styles to all matches. This is purely for convenience and doesn't create any special relationship between the selectors. Each selector maintains its own specificity.",
"diagram": "Selector List = OR Logic\n\np.note, li.important, #summary { ... }\n ↓ ↓ ↓\n Match OR Match OR Match\n ↓ ↓ ↓\n<p class=\"note\"> ✓ first matches\n<li class=\"important\"> ✓ second matches\n<div id=\"summary\"> ✓ third matches\n\nAll three get the same styles"
},
"validations": [
{
"type": "contains",
"value": "p.note",
"message": "Include <kbd>p.note</kbd> in your selector list",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "li.important",
"message": "Include <kbd>li.important</kbd> in your selector list",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "#summary",
"message": "Include <kbd>#summary</kbd> in your selector list",
"options": {
"caseSensitive": true
}
},
{
"type": "regex",
"value": "\\.card\\s+p\\s*\\{",
"message": "Use <kbd>.card p {</kbd> (space between .card and p)"
"value": "(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)",
"message": "Create a comma-separated list with all three selectors",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "background-color:",
"message": "Include the <kbd>background-color</kbd> property"
},
{
"type": "property_value",
"value": { "property": "font-size", "expected": "0.9rem" },
"message": "Set <kbd>font-size: 0.9rem</kbd>"
"value": {
"property": "background-color",
"expected": "lightyellow"
},
"message": "Set the background color to <kbd>lightyellow</kbd>"
},
{
"type": "contains",
"value": "border-left:",
"message": "Include the <kbd>border-left</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "border-left",
"expected": "3px solid gold"
},
"message": "Use <kbd>border-left: 3px solid gold</kbd> to create a left border"
},
{
"type": "contains",
"value": "padding-left:",
"message": "Include the <kbd>padding-left</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "padding-left",
"expected": "10px"
},
"message": "Use <kbd>padding-left: 10px</kbd> to add left padding"
}
]
},
{
"id": "universal-selector",
"title": "Universal (*)",
"description": "The universal selector is denoted by an asterisk (*) and matches any element of any type. It selects everything in the document or, when combined with other selectors, everything within a specific context. For example, <kbd>* { margin: 0; }</kbd> removes margins from all elements, while <kbd>article *</kbd> selects all elements inside article elements. When using the universal selector in combination with other selectors, you can apply properties like <kbd>margin</kbd> to control the spacing around elements. The universal selector is powerful but should be used carefully due to its broad impact. It's commonly used in CSS resets, to override default browser styling, or to target all children of a particular element.",
"task": "Use the universal selector to remove margins from all elements inside the container div. Create a rule using <kbd>div.container *</kbd> as the selector and set <kbd>margin: 0</kbd>.",
"previewHTML": "<div class=\"container\">\n <h2>Inside Container</h2>\n <p>This paragraph is inside the container.</p>\n <ul>\n <li>List item inside container</li>\n </ul>\n</div>\n<p>This paragraph is outside the container and should not be affected.</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } div.container { border: 2px solid navy; padding: 15px; background-color: lavender; } h2, p, ul, li { margin: 15px 0; }",
"sandboxCSS": "",
"codePrefix": "/* Use the universal selector to target all elements inside the container */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"concept": {
"explanation": "The universal selector * is a wildcard that matches every element type. When used alone, it affects the entire document. When combined with a descendant selector (like div.container *), it matches all descendants—children, grandchildren, and so on—regardless of element type. The space before * indicates a descendant relationship, not a direct parent-child relationship.",
"diagram": "Universal Selector as Wildcard\n\ndiv.container * { ... }\n ↑ ↑\n context wildcard (all descendants)\n\n<div class=\"container\">\n <h2> ← * matches this\n <p> ← * matches this\n <ul> ← * matches this\n <li> ← * matches this (nested!)\n</div>\n<p> ← NOT inside .container, no match"
},
"validations": [
{
"type": "regex",
"value": "^div\\.container\\s+\\*\\s*{",
"message": "Use <kbd>div.container *</kbd> selector (with a space between container and *)",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "margin:",
"message": "Include the <kbd>margin</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "margin",
"expected": "0"
},
"message": "Set margin to <kbd>0</kbd>"
},
{
"type": "regex",
"value": "div\\.container\\s+\\*\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "specificity-basics",
"title": "Specificity",
"description": "CSS specificity determines which styles take precedence when multiple conflicting rules target the same element. Specificity follows a hierarchical system: inline styles have the highest specificity, followed by ID selectors, then class/attribute/pseudo-class selectors, and finally element/pseudo-element selectors. This can be conceptualized as a four-part score (inline, ID, class, element). When creating multiple rules that may target the same elements, you can use the <kbd>color</kbd> property to set text colors, and specificity will determine which color is actually applied. Understanding specificity is crucial for predictable styling and debugging CSS conflicts. When two selectors have equal specificity, the one that comes last in the stylesheet wins.",
"task": "Examine the existing CSS rules and add a new rule with higher specificity to override the text color of the paragraph. Create a rule using '.content p' as the selector and set color: green.",
"previewHTML": "<div class=\"content\">\n <p>What color will this paragraph be? Look at the CSS rules and their specificity.</p>\n</div>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "p { border: 1px dashed gray; padding: 10px; }",
"codePrefix": "/* These CSS rules target the same paragraph but have different specificity */\n\n/* Rule 1: Element selector (lowest specificity) */\np {\n color: red;\n}\n\n/* Rule 2: Descendant selector (higher specificity than just 'p') */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"concept": {
"explanation": "When multiple rules match the same element, CSS uses specificity to decide which wins. Think of specificity as a point system: IDs are worth 100 points, classes 10 points, elements 1 point. The selector .content p (10 + 1 = 11 points) beats p (1 point), so green wins over red. This is the cascade in action—specificity determines which styles cascade down to the element.",
"diagram": "Specificity Point System\n\nSelector | Points | Color\n------------------+--------+-------\np | 1 | red\n.content p | 11 | green ← WINS!\n#main .content p | 111 | (would win over both)\n\nHigher points = wins the cascade\n\nThe <p> matches both rules, but:\n.content p has higher specificity → green"
},
"validations": [
{
"type": "regex",
"value": "^\\.content\\s+p\\s*{",
"message": "Use <kbd>.content p</kbd> as your selector (note the space between)",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "color:",
"message": "Include the <kbd>color</kbd> property"
},
{
"type": "contains",
"value": "green",
"message": "Set the color to <kbd>green</kbd>"
}
]
}

View File

@@ -1,17 +1,16 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "welcome",
"title": "Welcome",
"description": "Get started with Code Crispies",
"mode": "css",
"title": "Code Crispies",
"description": "Welcome to Code Crispies - your interactive web development learning platform",
"mode": "html",
"difficulty": "beginner",
"excludeFromProgress": true,
"lessons": [
{
"id": "hello",
"title": "Getting started!",
"description": "<strong>Welcome to Code Crispies!</strong> Learn CSS and HTML through hands-on exercises.<br><br><strong>How it works:</strong><br>1. Read the task on the left<br>2. Write code in the editor<br>3. See live results in the preview<br><br><strong>Tips:</strong> Use <kbd>Ctrl+Z</kbd> to undo. Open the menu (☰) to browse all modules.",
"task": "Write <code>Hello World</code> to get started",
"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>",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; font-size: 2rem; color: #6366f1; font-weight: bold; }",
"sandboxCSS": "",
@@ -25,6 +24,39 @@
"message": "Write <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/libretech/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",
"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": "",
"initialCode": "<p>Hello World! 🌍</p>\n<p>Hallo Welt!</p>\n<p>Bonjour le monde!</p>\n<p>¡Hola Mundo!</p>\n<p>Ciao Mondo!</p>\n<p>Olá Mundo!</p>\n<p>こんにちは世界!</p>\n<p>你好世界!</p>\n<p>안녕 세상!</p>\n<p>Привет мир!</p>\n<p dir=\"rtl\">שלום עולם!</p>\n<p dir=\"rtl\">مرحبا بالعالم!</p>",
"solution": "<p>Hello World! 🌍</p>\n<p>Hallo Welt!</p>\n<p>Bonjour le monde!</p>\n<p>¡Hola Mundo!</p>\n<p>Ciao Mondo!</p>\n<p>Olá Mundo!</p>\n<p>こんにちは世界!</p>\n<p>你好世界!</p>\n<p>안녕 세상!</p>\n<p>Привет мир!</p>\n<p dir=\"rtl\">שלום עולם!</p>\n<p dir=\"rtl\">مرحبا بالعالم!</p>",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "Hello",
"message": "Click Next to continue"
}
]
},
{
"id": "playground",
"title": "Playground",
"mode": "playground",
"description": "",
"task": "",
"previewHTML": "",
"previewBaseCSS": "",
"sandboxCSS": "",
"initialCode": "<style>\n body {\n font-family: system-ui, sans-serif;\n padding: 20px;\n }\n</style>\n\n<h1>Hello World</h1>\n<p>Start coding!</p>",
"solution": "",
"previewContainer": "preview-area",
"validations": []
}
]
}

View File

@@ -17,7 +17,7 @@
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "input[type=\"text\"] {\n background-color: lightblue;\n border: 2px solid blue;\n}",
"solution": "input[type=\"text\"] {\n background-color: lightblue;\n border: 2px solid blue\n}",
"validations": [
{
"type": "regex",

View File

@@ -7,183 +7,204 @@
"lessons": [
{
"id": "box-model-1",
"title": "Padding",
"description": "Every element in CSS is a box with four layers: content, padding, border, and margin. <strong>Padding</strong> creates breathing room between your content and the box's edge.<br><br>Without padding, text presses against borders awkwardly. Padding makes content readable and visually balanced.<br><br><pre>.card {\n padding: 1rem;\n}</pre>",
"task": "The text inside this profile card is pressed right against the edges. Give it some inner breathing room.",
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"codePrefix": ".box {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "padding: 1rem;",
"previewContainer": "preview-area",
"concept": {
"explanation": "Every HTML element is a rectangular box made of four concentric layers. The content sits at the center, padding creates breathing room inside the box (keeping content away from edges), border wraps around the padding, and margin pushes neighboring elements away. Think of it like a framed picture: the image is content, the matting is padding, the frame is the border, and the wall space around it is margin.",
"diagram": "CSS Box Model (4 Layers)\n\n┌─────────────────────────────┐\n│ Margin (transparent) │\n│ ┌────────────────────────┐ │\n│ │ Border │ │\n│ │ ┌──────────────────┐ │ │\n│ │ │ Padding (inside) │ │ │\n│ │ │ ┌────────────┐ │ │ │\n│ │ │ │ Content │ │ │ │\n│ │ │ │ Area │ │ │ │\n│ │ │ └────────────┘ │ │ │\n│ │ └──────────────────┘ │ │\n│ └────────────────────────┘ │\n└─────────────────────────────┘"
},
"validations": [
{
"type": "property_value",
"value": { "property": "padding", "expected": "1rem" },
"message": "Which property adds space between content and the element's edge?"
"message": "Set <kbd>padding: 1rem</kbd>"
}
]
},
{
"id": "box-model-2",
"title": "Borders",
"description": "Borders create visual boundaries around elements. The <kbd>border</kbd> shorthand takes three values: width, style, and color.<br><br>Common styles: <kbd>solid</kbd>, <kbd>dashed</kbd>, <kbd>dotted</kbd>, <kbd>none</kbd>",
"task": "This card could use a colored accent line along its left edge.",
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"codePrefix": ".box {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "border-left: 4px solid steelblue;",
"solution": "border: 2px solid darkslategray;",
"previewContainer": "preview-area",
"concept": {
"explanation": "Borders sit between padding and margin, defining the visual edge of an element. They're unique in the box model because they're visible by default (unlike transparent padding and margin). The border shorthand combines three properties—width, style, and color—into one declaration. Borders add to an element's total size unless you use box-sizing: border-box.",
"diagram": "Border Position in Box Model\n\n┌─────────────────────┐\n│ Margin │ (outside)\n│ ╔═══════════════╗ │\n│ ║ Border (2px) ║ │ ← You are here\n│ ║ ┌─────────┐ ║ │\n│ ║ │ Padding │ ║ │\n│ ║ │ Content │ ║ │\n│ ║ └─────────┘ ║ │\n│ ╚═══════════════╝ │\n└─────────────────────┘"
},
"validations": [
{
"type": "regex",
"value": "border-left:\\s*4px\\s+solid\\s+steelblue",
"message": "Use the shorthand that sets a border on just one side",
"value": "border:\\s*2px\\s+solid\\s+darkslategray",
"message": "Set <kbd>border: 2px solid darkslategray</kbd>",
"options": { "caseSensitive": false }
}
]
},
{
"id": "box-model-3",
"title": "Margins",
"description": "Margins create space <em>outside</em> the element, separating it from neighbors. While padding pushes content inward, margins push other elements away.",
"task": "These two profile cards are touching each other. Add some space below each card to separate them.",
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article><article class=\"card\"><h3>Alex Rivera</h3><p>UX Designer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"codePrefix": ".outer {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "margin-bottom: 1rem;",
"solution": "margin: 1rem;",
"previewContainer": "preview-area",
"concept": {
"explanation": "Margins are the invisible space outside an element's border that pushes other elements away. Unlike padding (which is inside the border and gets the background color), margins are always transparent. They don't affect the element's own size—they control the relationship between elements. Margins can even be negative, pulling elements closer together or overlapping them.",
"diagram": "Margin vs Padding\n\n┌───────────────────────────┐\n│ ░░░░░ MARGIN ░░░░░ │ (transparent)\n│ ░ ┌─────────────────┐ ░ │\n│ ░ │ BORDER │ ░ │\n│ ░ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ ░ │\n│ ░ │ ▓ PADDING ▓ │ ░ │ (gets background)\n│ ░ │ ▓ ┌─────────┐ ▓ │ ░ │\n│ ░ │ ▓ │ CONTENT │ ▓ │ ░ │\n│ ░ │ ▓ └─────────┘ ▓ │ ░ │\n│ ░ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ ░ │\n│ ░ └─────────────────┘ ░ │\n│ ░░░░░░░░░░░░░░░░░░░░░░░ │\n└───────────────────────────┘"
},
"validations": [
{
"type": "property_value",
"value": { "property": "margin-bottom", "expected": "1rem" },
"message": "Which property pushes neighboring elements away from the bottom?"
"value": { "property": "margin", "expected": "1rem" },
"message": "Set <kbd>margin: 1rem</kbd>"
}
]
},
{
"id": "box-model-4",
"title": "Box Sizing",
"description": "By default, <kbd>width</kbd> only sets the content width. Padding and borders add to the total. This causes layout headaches.<br><br><kbd>box-sizing: border-box</kbd> includes padding and border in the width, making sizing predictable. Most developers apply this to all elements.",
"task": "Both cards are set to the same width, but the left one overflows because padding and border are added on top. Fix the right card so its size includes padding and border.",
"previewHTML": "<div class=\"demo\"><article class=\"card\">Content-box</article><article class=\"card fix\">Border-box</article></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .demo { display: flex; gap: 1rem; } .card { width: 200px; padding: 1rem; border: 4px solid steelblue; background: white; border-radius: 8px; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".fix {\n ",
"codePrefix": ".sized {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "box-sizing: border-box;",
"previewContainer": "preview-area",
"concept": {
"explanation": "By default (content-box), when you set width: 200px, the browser makes only the content 200px wide, then adds padding and border on top—making the total width larger. With border-box, the browser makes the entire box 200px including padding and border, shrinking the content area to fit. Border-box makes layout math predictable: width: 200px means the element is exactly 200px wide, period.",
"diagram": "content-box vs border-box\n\nwidth: 200px + padding: 20px + border: 4px\n\ncontent-box (default):\n┌────────────────────────────┐\n│ Border (4px) │\n│ ┌──────────────────────┐ │\n│ │ Padding (20px) │ │\n│ │ ┌────────────────┐ │ │\n│ │ │ Content 200px │ │ │ Total: 248px!\n│ │ └────────────────┘ │ │\n│ └──────────────────────┘ │\n└────────────────────────────┘\n\nborder-box:\n┌──────────────────────┐\n│ Border + Padding │\n│ ┌────────────────┐ │\n│ │ Content ~152px │ │ Total: 200px ✓\n│ └────────────────┘ │\n└──────────────────────┘"
},
"validations": [
{
"type": "property_value",
"value": { "property": "box-sizing", "expected": "border-box" },
"message": "Which sizing mode includes padding and border in the element's width?"
"message": "Set <kbd>box-sizing: border-box</kbd>"
}
]
},
{
"id": "box-model-5",
"title": "Padding Shorthand",
"description": "Padding accepts 1-4 values:<br>• 1 value: all sides<br>• 2 values: vertical | horizontal<br>• 4 values: top | right | bottom | left",
"task": "This button feels too tight. Give it more space on the sides than on top and bottom, using the two-value shorthand.",
"previewHTML": "<button class=\"btn\">Follow</button>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".btn {\n ",
"codePrefix": ".first {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "padding: 8px 1rem;",
"solution": "margin-bottom: 2rem;",
"previewContainer": "preview-area",
"concept": {
"explanation": "When two vertical margins touch, they don't add up—they collapse into a single margin equal to the larger of the two. This prevents excessive spacing between stacked elements like paragraphs. If you have margin-bottom: 2rem and margin-top: 1rem, the total space is 2rem (not 3rem). Horizontal margins never collapse; only vertical ones do.",
"diagram": "Vertical Margin Collapse\n\nWithout collapse (expected?):\n┌─────────────┐\n│ Element 1 │\n└─────────────┘\n ↓ 2rem margin-bottom\n ↓ 1rem margin-top\n┌─────────────┐ Total: 3rem?\n│ Element 2 │\n└─────────────┘\n\nWith collapse (actual):\n┌─────────────┐\n│ Element 1 │\n└─────────────┘\n ↓\n ↓ 2rem (larger wins)\n ↓\n┌─────────────┐ Total: 2rem ✓\n│ Element 2 │\n└─────────────┘"
},
"validations": [
{
"type": "regex",
"value": "padding:\\s*8px\\s+1rem",
"message": "Use the two-value shorthand: vertical first, then horizontal",
"options": { "caseSensitive": false }
"type": "property_value",
"value": { "property": "margin-bottom", "expected": "2rem" },
"message": "Set <kbd>margin-bottom: 2rem</kbd>"
}
]
},
{
"id": "box-model-6",
"title": "Margin Shorthand",
"description": "Margin uses the same shorthand pattern as padding. A common pattern is centering block elements horizontally with <kbd>margin: 0 auto</kbd>.",
"task": "This card is stuck to the left. Center it horizontally using the margin shorthand with auto side margins.",
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { width: 250px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"codePrefix": ".spaced {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "margin: 0 auto;",
"solution": "margin: 1rem 2rem;",
"previewContainer": "preview-area",
"concept": {
"explanation": "The margin shorthand uses a clockwise pattern: one value applies to all sides, two values set vertical then horizontal, three values set top, horizontal, then bottom, and four values go clockwise from top (top, right, bottom, left). The two-value pattern is most common because vertical and horizontal spacing often differ. Think 'Y-axis first, X-axis second.'",
"diagram": "Margin Shorthand Patterns\n\nOne value:\nmargin: 1rem;\n → all sides: 1rem\n\nTwo values:\nmargin: 1rem 2rem;\n ↓ ↓\n vertical horizontal\n (Y) (X)\n\nFour values (clockwise):\nmargin: 1rem 2rem 3rem 4rem;\n ↓ ↓ ↓ ↓\n top right bottom left\n T R B L"
},
"validations": [
{
"type": "regex",
"value": "margin:\\s*0\\s+auto",
"message": "Use the shorthand that auto-calculates equal horizontal margins",
"value": "margin:\\s*1rem\\s+2rem",
"message": "Set <kbd>margin: 1rem 2rem</kbd>",
"options": { "caseSensitive": false }
}
]
},
{
"id": "box-model-7",
"title": "Border Radius",
"description": "While not part of the classic box model, <kbd>border-radius</kbd> rounds the corners of an element's border box. Use <kbd>50%</kbd> on a square element to create a circle.",
"task": "The square avatar image should appear as a perfect circle.",
"previewHTML": "<article class=\"card\"><img class=\"avatar\" src=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='35' r='25' fill='%23666'/%3E%3Ccircle cx='50' cy='90' r='40' fill='%23666'/%3E%3C/svg%3E\" alt=\"Avatar\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; text-align: center; } .avatar { width: 80px; height: 80px; background: #ddd; margin-bottom: 8px; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".avatar {\n ",
"codePrefix": ".padded {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "border-radius: 50%;",
"solution": "padding: 2rem;",
"previewContainer": "preview-area",
"concept": {
"explanation": "Padding shorthand follows the same clockwise pattern as margin: one value for all sides, two values for vertical/horizontal, four values for top/right/bottom/left. The key difference is that padding creates internal space (pushing content away from borders) while margin creates external space (pushing other elements away). Padding also inherits the element's background color.",
"diagram": "Padding Shorthand (same pattern)\n\npadding: 2rem;\n → Equal on all sides\n\n┌─────────────────────┐\n│ Border │\n│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ ← 2rem padding\n│ ▓ ┌───────────┐ ▓ │ (all sides)\n│ ▓ │ Content │ ▓ │\n│ ▓ └───────────┘ ▓ │\n│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │\n└─────────────────────┘"
},
"validations": [
{
"type": "property_value",
"value": { "property": "border-radius", "expected": "50%" },
"message": "Which property rounds corners? Think about what percentage makes a circle"
"value": { "property": "padding", "expected": "2rem" },
"message": "Set <kbd>padding: 2rem</kbd>"
}
]
},
{
"id": "box-model-8",
"title": "Complete Card",
"description": "Let's combine everything. This notification card needs styling to look polished.",
"task": "This notification needs three things: inner spacing so text isn't cramped, a colored accent on the left edge, and slightly rounded corners.",
"previewHTML": "<div class=\"alert\"><strong>New message</strong><p>You have 3 unread notifications</p></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { background: seashell; } .alert strong { color: coral; } .alert p { margin: 4px 0 0; color: #666; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".alert {\n ",
"codePrefix": ".line {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "padding: 1rem;\n border-left: 4px solid coral;\n border-radius: 4px;",
"solution": "border-bottom: 4px solid dodgerblue;",
"previewContainer": "preview-area",
"concept": {
"explanation": "You can apply borders to individual sides using border-top, border-right, border-bottom, or border-left. This is common for creating underlines, dividers, or asymmetric designs. Each side can have different widths, styles, and colors. The shorthand border property is just a convenience—underneath, the browser sets all four sides at once.",
"diagram": "Individual Border Sides\n\nborder-bottom only:\n┌──────────────────┐\n│ │ (no border)\n│ Content │\n│ │\n└══════════════════┘ ← border-bottom\n\nMix and match:\nborder-left + border-bottom:\n ┌─────────────────┐\n ║ │\n ║ Content │\n ║ │\n ╚═════════════════╝"
},
"validations": [
{
"type": "property_value",
"value": { "property": "padding", "expected": "1rem" },
"message": "Add inner spacing to the notification"
},
{
"type": "regex",
"value": "border-left:\\s*4px\\s+solid\\s+coral",
"message": "Add a colored accent on the left edge",
"value": "border-bottom:\\s*4px\\s+solid\\s+dodgerblue",
"message": "Set <kbd>border-bottom: 4px solid dodgerblue</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "border-radius", "expected": "4px" },
"message": "Soften the corners of the notification"
}
]
}

View File

@@ -17,6 +17,10 @@
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"concept": {
"explanation": "Element selectors work by matching the tag name against every node in the DOM tree. The browser traverses the entire document, checking each element's type. When it finds a match, it applies the styles. This is why p selects ALL paragraph elements—the browser doesn't stop after the first match. Element selectors have the lowest specificity (0,0,0,1), making them easy to override with classes or IDs.",
"diagram": "Browser DOM Traversal\n\n<html>\n <body>\n <h1> ← Check: is this a <p>? NO\n <p> ← Check: is this a <p>? YES ✓ Apply styles\n <div> ← Check: is this a <p>? NO\n <p> ← Check: is this a <p>? YES ✓ Apply styles\n </body>\n</html>\n\nSpecificity: 0,0,0,1 (lowest)"
},
"validations": [
{ "type": "contains", "value": "p {", "message": "Use the element selector <kbd>p</kbd>", "options": { "caseSensitive": false } },
{ "type": "contains", "value": "color", "message": "Include the <kbd>color</kbd> property", "options": { "caseSensitive": false } },
@@ -40,6 +44,10 @@
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"concept": {
"explanation": "Class selectors match elements based on their class attribute, not their tag type. This is powerful because multiple elements of different types can share the same class. The browser checks each element's class attribute for a match—it doesn't care if it's an h2, div, or span. Class selectors have medium specificity (0,0,1,0), which is 10x higher than element selectors, allowing them to override type-based styles.",
"diagram": "Class Attribute Matching\n\n.title { color: blueviolet; }\n ↓\nBrowser searches for class=\"title\"\n ↓\n<h2 class=\"title\"> ✓ MATCH (class=\"title\")\n<h2> ✗ no class attribute\n<div class=\"title\"> ✓ MATCH (different type, same class!)\n\nSpecificity: 0,0,1,0\n(10x stronger than element selectors)"
},
"validations": [
{
"type": "contains",
@@ -67,6 +75,10 @@
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"concept": {
"explanation": "ID selectors match a single unique element by its id attribute. IDs must be unique per page, so #description can only match one element maximum. ID selectors have very high specificity (0,1,0,0)—100x stronger than class selectors! This makes IDs powerful but dangerous: their high specificity makes them hard to override later, which is why many developers prefer classes for styling.",
"diagram": "ID High Specificity\n\n#description { color: orangered; }\n ↓\nSearches for id=\"description\" (unique!)\n ↓\n<div id=\"description\"> ✓ MATCH (only this one)\n<div> ✗ no id\n<div id=\"other\"> ✗ different id\n\nSpecificity Comparison:\n ID: 0,1,0,0 (100 points)\n Class: 0,0,1,0 (10 points)\n Element: 0,0,0,1 (1 point)\n\nID wins almost all conflicts!"
},
"validations": [
{
"type": "contains",
@@ -94,6 +106,10 @@
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"concept": {
"explanation": "Combining selectors creates AND logic—both conditions must be true. The selector div.note (no space between!) requires the element to be a div AND have class=\"note\". Combining also adds specificity: div.note = 0,0,1,1 (element + class), which beats .note alone = 0,0,1,0. This specificity cascade is how CSS resolves conflicts when multiple rules target the same element—higher specificity always wins.",
"diagram": "Combined Selector AND Logic\n\ndiv.note { ... }\n ↑ ↑ no space = BOTH required\n │ └─ class=\"note\"\n └───── element type <div>\n\n<div class=\"note\"> ✓ MATCH (div AND class)\n<p class=\"note\"> ✗ wrong element type\n<div> ✗ missing class\n\nSpecificity Addition:\n div.note = 0,0,1,1 (11 points)\n .note = 0,0,1,0 (10 points)\n div = 0,0,0,1 (1 point)\n\nCombining selectors = higher specificity!"
},
"validations": [
{
"type": "contains",

View File

@@ -1,94 +1,134 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "colors-backgrounds",
"title": "CSS Colors",
"description": "Learn how to apply colors to text and backgrounds using CSS properties.",
"title": "Colors",
"description": "Learn how to apply and manipulate colors, backgrounds, and graphical fills using CSS properties.",
"difficulty": "beginner",
"lessons": [
{
"id": "colors-1",
"title": "Background Color",
"description": "Color is one of the most powerful tools in web design. It creates visual hierarchy, conveys meaning, and establishes brand identity. CSS provides multiple ways to specify colors.<br><br><strong>CSS named colors:</strong> CSS includes 147 named colors like <kbd>steelblue</kbd>, <kbd>coral</kbd>, <kbd>gold</kbd>, and <kbd>tomato</kbd>. These are easy to remember and read.<br><br><strong>The background-color property:</strong> Sets the fill color behind an element's content and padding areas.<br><br><pre>.card {\n background-color: lightblue;\n}</pre>",
"task": "This notification card looks bare. Give it a soft, warm background color.",
"previewHTML": "<div class=\"alert\"><strong>New message</strong><p>You have 3 unread notifications</p></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { padding: 1rem; border-left: 4px solid coral; border-radius: 4px; } .alert strong { display: block; margin-bottom: 4px; } .alert p { margin: 0; color: #666; font-size: 0.9rem; }",
"title": "Setting Background Colors",
"description": "Use the <code>background-color</code> property to fill elements with solid colors.",
"task": "Apply a light cyan background (#e0f7fa) to the element with class 'colorbox'.",
"previewHTML": "<div class=\"colorbox\">Background Demo</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .colorbox { padding: 1rem; }",
"sandboxCSS": "",
"codePrefix": ".alert {\n ",
"codePrefix": "/* Set a background color */\n.colorbox {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "background-color: seashell;",
"codeSuffix": "}",
"previewContainer": "preview-area",
"concept": {
"explanation": "Hexadecimal color codes represent RGB (Red, Green, Blue) values using base-16 counting. The format #RRGGBB uses two digits for each color channel (00-FF in hex = 0-255 in decimal). For example, #e0f7fa means red=224, green=247, blue=250. Hex is popular because it's compact—6 characters can represent 16.7 million colors. Web developers prefer hex for consistency across browsers and ease of copy-pasting from design tools.",
"diagram": "Hex Color Breakdown: #e0f7fa\n\n#e0f7fa\n ││││││\n ││└┴── Blue (fa = 250) High blue\n │└──── Green (f7 = 247) High green\n └───── Red (e0 = 224) Medium-high red\n\nResult: Light cyan (lots of green+blue)\n\nCommon formats compared:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nHex: #e0f7fa (compact)\nRGB: rgb(224, 247, 250) (readable)\nHSL: hsl(187, 71%, 93%) (intuitive)\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
},
"validations": [
{ "type": "contains", "value": ".colorbox", "message": "Select <kbd>.colorbox</kbd>", "options": { "caseSensitive": false } },
{
"type": "regex",
"value": "background-color:\\s*(seashell|linen|mistyrose|lavenderblush|cornsilk|oldlace|papayawhip|antiquewhite|bisque|peachpuff)",
"message": "Which property fills the area behind the content? Try a warm, soft color name",
"type": "contains",
"value": "background-color",
"message": "Use <kbd>background-color</kbd> property",
"options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "background-color", "expected": "#e0f7fa" },
"message": "Set background-color to <kbd>#e0f7fa</kbd>",
"options": { "exact": true }
}
]
},
{
"id": "colors-2",
"title": "Text Color",
"description": "The <kbd>color</kbd> property sets the color of text content. Good contrast between text and background is essential for readability and accessibility.",
"task": "The alert title blends in with the body text. Make it pop with a warm accent color.",
"previewHTML": "<div class=\"alert\"><strong class=\"title\">Warning</strong><p>Your session will expire in 5 minutes</p></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { padding: 1rem; background-color: seashell; border-left: 4px solid coral; border-radius: 4px; } .alert .title { display: block; margin-bottom: 4px; } .alert p { margin: 0; color: #666; font-size: 0.9rem; }",
"title": "Text Color and Contrast",
"description": "Apply the <code>color</code> property to control text readability against backgrounds.",
"task": "Set the text color of '.colorbox' to deep blue (#01579b). Ensure good contrast.",
"previewHTML": "<div class=\"colorbox\">Color & Contrast</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .colorbox { padding: 1rem; background: #e0f7fa; }",
"sandboxCSS": "",
"codePrefix": ".title {\n ",
"codePrefix": "/* Set text color */\n.colorbox {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "color: coral;",
"codeSuffix": "}",
"previewContainer": "preview-area",
"concept": {
"explanation": "Color contrast is the difference in brightness between text and background, measured as a ratio from 1:1 (invisible) to 21:1 (black on white). WCAG accessibility guidelines require at least 4.5:1 for normal text and 3:1 for large text to ensure readability for people with vision impairments. Dark blue (#01579b) on light cyan (#e0f7fa) provides excellent contrast (~8.2:1) because there's significant brightness difference. Using HSL format helps choose contrasting colors: keep the same hue but vary lightness (L) dramatically.",
"diagram": "Contrast Ratio Comparison\n\nBackground: #e0f7fa (light cyan)\n\nText Options:\n┌────────────────────────────┐\n│ #01579b (dark blue) │ 8.2:1 ✓ Excellent\n│ #0288d1 (medium blue) │ 3.8:1 ✗ Fails WCAG\n│ #b3e5fc (light blue) │ 1.2:1 ✗ Unreadable\n└────────────────────────────┘\n\nWCAG Requirements:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━\nNormal text: 4.5:1 minimum\nLarge text: 3.0:1 minimum\nAA Standard: Good for most\nAAA Standard: 7.0:1 (ideal)\n━━━━━━━━━━━━━━━━━━━━━━━━━━━"
},
"validations": [
{ "type": "contains", "value": ".colorbox", "message": "Select <kbd>.colorbox</kbd>", "options": { "caseSensitive": false } },
{ "type": "contains", "value": "color", "message": "Use the <kbd>color</kbd> property", "options": { "caseSensitive": false } },
{
"type": "regex",
"value": "color:\\s*(coral|tomato|orangered|indianred|salmon|darksalmon)",
"message": "Which property changes the text color? Try a warm, vibrant color name",
"options": { "caseSensitive": false }
"type": "property_value",
"value": { "property": "color", "expected": "#01579b" },
"message": "Set color to <kbd>#01579b</kbd>",
"options": { "exact": true }
}
]
},
{
"id": "colors-3",
"title": "Border Color",
"description": "Borders can have their own color using <kbd>border-color</kbd>. This is useful when you want to change just the color without redefining the entire border.",
"task": "The card border is dull gray. Give it a warm accent color.",
"previewHTML": "<article class=\"card\"><h3>Premium Plan</h3><p>Unlimited access to all features</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { padding: 1rem; background: white; border: 4px solid #ddd; border-radius: 8px; } .card h3 { margin: 0 0 8px; } .card p { margin: 0; color: #666; }",
"title": "CSS Gradients",
"description": "Learn to create smooth transitions between colors using linear and radial gradients.",
"task": "Apply a linear gradient background from #ff9a9e to #fad0c4 on an element with class 'gradient-box'.",
"previewHTML": "<div class=\"gradient-box\">Gradient Demo</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .gradient-box { padding: 1rem; color: white; text-align: center; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"codePrefix": "/* Set a linear gradient background */\n.gradient-box {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "border-color: coral;",
"codeSuffix": "}",
"previewContainer": "preview-area",
"concept": {
"explanation": "CSS gradients work by interpolating (smoothly transitioning) between color values at different positions called \"color stops\". The browser calculates hundreds of intermediate colors between your specified stops, blending RGB values proportionally. Linear gradients transition along a straight line (default: top to bottom), while radial gradients emanate from a center point. Gradients are actually generated images, which is why they use background-image instead of background-color. You can combine multiple gradients and control their direction, shape, and stop positions for complex effects.",
"diagram": "Linear Gradient Interpolation\n\nlinear-gradient(#ff9a9e, #fad0c4)\n\n 0% ┌─────────────────┐\n │ #ff9a9e (pink) │ ← Start color\n ├─────────────────┤\n 25% │ #ffb0ad │ ↓\n ├─────────────────┤ Browser\n 50% │ #ffc3b8 │ calculates\n ├─────────────────┤ intermediate\n 75% │ #ffd5c3 │ colors\n ├─────────────────┤ ↓\n100% │ #fad0c4 (peach) │ ← End color\n └─────────────────┘\n\nDirection options:\nto bottom (default), to right,\nto top, 45deg, 180deg, etc."
},
"validations": [
{ "type": "contains", "value": ".gradient-box", "message": "Select <kbd>.gradient-box</kbd>", "options": { "caseSensitive": false } },
{
"type": "contains",
"value": "background-image",
"message": "Use <kbd>background-image</kbd> property",
"options": { "caseSensitive": false }
},
{
"type": "regex",
"value": "border-color:\\s*(coral|tomato|orangered|indianred|salmon|darksalmon|crimson)",
"message": "Which property changes just the border's color? Try a warm, vibrant name",
"value": "linear-gradient\\(.*#ff9a9e.*,.*#fad0c4.*\\)",
"message": "Use <kbd>linear-gradient</kbd> from <kbd>#ff9a9e</kbd> to <kbd>#fad0c4</kbd>",
"options": { "caseSensitive": false }
}
]
},
{
"id": "colors-4",
"title": "Hex Colors",
"description": "Beyond named colors, CSS supports hex codes (<kbd>#ff6347</kbd>), RGB (<kbd>rgb(255, 99, 71)</kbd>), and HSL (<kbd>hsl(9, 100%, 64%)</kbd>). Hex codes are the most common format in professional projects.",
"task": "This badge needs a golden background. Use a hex color code to set it.",
"previewHTML": "<span class=\"badge\">NEW</span>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .badge { display: inline-block; padding: 4px 12px; border-radius: 999px; font-size: 0.75rem; font-weight: bold; text-transform: uppercase; color: #333; }",
"title": "Background Images & Repeat",
"description": "Add images as backgrounds and control repetition and positioning.",
"task": "Set a background image on '.bg-img' using a placeholder URL, center it, and prevent tiling.",
"previewHTML": "<div class=\"bg-img\">Image Background</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .bg-img { height: 150px; display: flex; align-items: center; justify-content: center; color: white; }",
"sandboxCSS": "",
"codePrefix": ".badge {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "background-color: #ffd700;",
"codePrefix": "/* Set background image */\n\n.bg-img {",
"initialCode": " background-image: url('http://placekitten.com/320/320');\n background-position: center; background-repeat: no-repeat;\n ",
"codeSuffix": "}",
"previewContainer": "preview-area",
"concept": {
"explanation": "Background images layer behind content and can be combined with background colors—the color shows through transparent areas of the image or when the image doesn't cover the full element. By default, background images tile (repeat) to fill the entire element, mimicking wallpaper patterns. The background-position property uses a coordinate system where 'center' means 50% 50%, and you can use keywords (top, left), percentages, or exact pixel values. Setting background-repeat: no-repeat displays the image once, useful for logos or hero images.",
"diagram": "Background Layers (front to back)\n\n┌────────────────────────────┐\n│ ┌──────────┐ │\n│ │ Content │ │ ← Layer 4\n│ └──────────┘ │\n│ ╔════════════════════╗ │\n│ ║ background-image ║ │ ← Layer 3\n│ ║ (photo/pattern) ║ │\n│ ╚════════════════════╝ │\n│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│ ← Layer 2\n│▓ background-color (solid) ▓│ (shows through\n│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│ transparent areas)\n└────────────────────────────┘\n│ Parent background │ ← Layer 1\n\nRepeat options:\nrepeat (default), no-repeat,\nrepeat-x, repeat-y, space, round"
},
"validations": [
{
"type": "regex",
"value": "background-color:\\s*(#ffd700|#ffcc00|#ffc107|#f0c000|gold)",
"message": "Use a hex code for background-color — something in the gold/yellow family",
"type": "contains",
"value": "background-image",
"message": "Use <kbd>background-image</kbd> property",
"options": { "caseSensitive": false }
},
{
"type": "contains",
"value": "background-position: center",
"message": "Center the background image with <kbd>background-position: center</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "contains",
"value": "background-repeat: no-repeat",
"message": "Prevent image tiling with <kbd>background-repeat: no-repeat</kbd>",
"options": { "caseSensitive": false }
}
]

View File

@@ -2,70 +2,105 @@
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "typography-fonts",
"title": "Typography",
"description": "Learn how to control text appearance through font selection, sizing, spacing, and styling.",
"description": "Learn how to control text appearance through font selection, sizing, spacing, and decorative effects.",
"difficulty": "beginner",
"lessons": [
{
"id": "monospace-font",
"title": "Monospace Fonts",
"description": "Different font families create different visual impressions. <kbd>monospace</kbd> fonts have equal-width characters, making them perfect for displaying code, data, or keyboard shortcuts.<br><br>Every computer has a monospace font built in, so <kbd>font-family: monospace</kbd> always works. This makes it a safe choice for inline code in any web project.",
"task": "Style the inline code with a monospace font. Add <kbd>font-family: monospace</kbd>.",
"previewHTML": "<article>\n <h3>Quick Tip</h3>\n <p>Press <span class=\"code\">Ctrl + S</span> to save your work, or <span class=\"code\">Ctrl + Z</span> to undo.</p>\n</article>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } h3 { margin: 0 0 0.5rem; } p { margin: 0; color: #555; line-height: 1.6; } .code { background: #f0f0f0; padding: 0.2rem 0.4rem; border-radius: 4px; }",
"id": "typography-1",
"title": "Font Family & Fallbacks",
"description": "Specify custom fonts and reliable fallback stacks for consistent typography across devices.",
"task": "Set the <code>font-family</code> of the '.text' element to 'Georgia, serif' with serif fallback.",
"previewHTML": "<p class=\"text\">This text shows the chosen font family.</p>",
"previewBaseCSS": "body { padding: 1rem; }",
"sandboxCSS": "",
"codePrefix": ".code {\n ",
"codePrefix": "/* Set font family */\n.text {",
"initialCode": "",
"codeSuffix": "\n}",
"codeSuffix": "}",
"previewContainer": "preview-area",
"solution": "font-family: monospace;",
"concept": {
"explanation": "Font stacks are comma-separated lists that provide fallback options when fonts aren't available on a user's device. Browsers try each font from left to right until they find one installed locally. 'Georgia' is a web-safe font (pre-installed on most systems), and 'serif' is a generic family keyword that tells the browser to use any serif font if Georgia fails. This progressive fallback ensures text always displays in a readable font, even when custom fonts fail to load or aren't supported.",
"diagram": "Font Stack Resolution Process\n\nfont-family: Georgia, serif;\n\n1. Try Georgia\n ┌─────────────────┐\n │ Is Georgia │ YES → Use Georgia ✓\n │ installed? │\n └─────────────────┘\n │ NO\n ↓\n2. Try serif (generic)\n ┌─────────────────┐\n │ Use browser's │ → Times, Times New Roman,\n │ default serif │ or similar serif font\n └─────────────────┘\n\nCommon web-safe fonts:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nSerif: Georgia, Times\nSans-serif: Arial, Helvetica, Verdana\nMonospace: Courier, Courier New\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nGeneric families (always available):\nserif, sans-serif, monospace,\ncursive, fantasy, system-ui"
},
"validations": [
{
"type": "property_value",
"value": { "property": "font-family", "expected": "monospace" },
"message": "Set font-family to <kbd>monospace</kbd>"
"type": "contains",
"value": "font-family",
"message": "Use the <kbd>font-family</kbd> property",
"options": { "caseSensitive": false }
},
{
"type": "regex",
"value": "Georgia, serif",
"message": "Include <kbd>Georgia, serif</kbd> in the font stack",
"options": { "caseSensitive": false }
}
]
},
{
"id": "font-size-line-height",
"title": "Readable Body Text",
"description": "Good typography is invisible—readers focus on content, not struggling to read it. Two properties work together for readability:<br><br><kbd>font-size</kbd> controls how big the text appears. For body text, <kbd>1rem</kbd> (16px by default) is a comfortable starting point.<br><br><kbd>line-height</kbd> sets the space between lines. A value of <kbd>1.6</kbd> means the line height is 1.6 times the font size—giving text room to breathe.",
"task": "Make the text comfortable to read. Add <kbd>font-size: 1rem</kbd> and <kbd>line-height: 1.6</kbd>.",
"previewHTML": "<article>\n <h2>The Art of Coffee</h2>\n <p>Great coffee starts with quality beans. The roast level affects flavor—light roasts are fruity and acidic, while dark roasts are bold and smoky.</p>\n <p>Water temperature matters too. Brew between 90-96°C for optimal extraction. Too hot burns the coffee; too cold leaves it weak.</p>\n</article>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } h2 { margin: 0 0 1rem; color: #333; } p { margin: 0 0 1rem; color: #444; } p:last-child { margin-bottom: 0; }",
"id": "typography-2",
"title": "Font Size & Line Height",
"description": "Control text scale and readability by adjusting size and line heights.",
"task": "Set the heading '.heading' to 1.5rem font-size and 1.5 line-height.",
"previewHTML": "<h2 class=\"heading\">Readable Heading</h2>",
"previewBaseCSS": "body { padding: 1rem; }",
"sandboxCSS": "",
"codePrefix": "article {\n ",
"codePrefix": "/* Set size and line height */\n.heading {",
"initialCode": "",
"codeSuffix": "\n}",
"codeSuffix": "}",
"previewContainer": "preview-area",
"solution": "font-size: 1rem;\n line-height: 1.6;",
"concept": {
"explanation": "Browsers render text sizes using the rem unit (root em), which is relative to the root element's font size (typically 16px by default). 1.5rem equals 24px on most browsers (16px × 1.5). Line-height controls the vertical space allocated for each line—1.5 means each line gets 1.5× the font size in height (36px total for 24px text). Unitless line-height values are preferred because they scale proportionally when font-size changes, maintaining consistent readability. Optimal line-height for body text is usually 1.4-1.6, while headings work well at 1.1-1.3.",
"diagram": "How Browsers Calculate Text Spacing\n\nfont-size: 1.5rem (24px) line-height: 1.5\n\nRoot font-size (html): 16px\n ↓\n 16px × 1.5 = 24px font-size\n ↓\n 24px × 1.5 = 36px line-height\n\nVisual spacing:\n\n┌────────────────────────────┐\n│ ↕ 6px (leading above) │\n├────────────────────────────┤\n│ Readable Heading (24px) │ ← Text\n├────────────────────────────┤\n│ ↕ 6px (leading below) │\n└────────────────────────────┘\n Total: 36px (line-height)\n\nLeading = (line-height - font-size) / 2\n = (36px - 24px) / 2 = 6px\n\nCommon line-height values:\n━━━━━━━━━━━━━━━━━━━━━━━━\nBody text: 1.5 - 1.6\nHeadings: 1.1 - 1.3\nSingle-line: 1.0 (tight)\nPoetry/code: 1.8 - 2.0"
},
"validations": [
{ "type": "contains", "value": "font-size", "message": "Use <kbd>font-size</kbd> property", "options": { "caseSensitive": false } },
{
"type": "property_value",
"value": { "property": "font-size", "expected": "1rem" },
"message": "Set font-size to <kbd>1rem</kbd>"
"value": { "property": "font-size", "expected": "1.5rem" },
"message": "Set font-size to <kbd>1.5rem</kbd>"
},
{
"type": "contains",
"value": "line-height",
"message": "Use <kbd>line-height</kbd> property",
"options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "line-height", "expected": "1.6" },
"message": "Set line-height to <kbd>1.6</kbd>"
"value": { "property": "line-height", "expected": "1.5" },
"message": "Set line-height to <kbd>1.5</kbd>"
}
]
},
{
"id": "font-weight",
"title": "Bold Headings",
"description": "Font weight controls how thick or thin text appears. Common values include:<br><br>• <kbd>normal</kbd> (400) — default body text<br>• <kbd>bold</kbd> (700) — strong emphasis<br>• <kbd>lighter</kbd> / <kbd>bolder</kbd> — relative to parent<br><br>Card titles and headings often use bold text to create visual hierarchy and draw attention to key information.",
"task": "Make the card title stand out. Add <kbd>font-weight: bold</kbd>.",
"previewHTML": "<article class=\"card\">\n <span class=\"title\">Weekend Special</span>\n <p>Fresh pastries and artisan coffee, 20% off all items this Saturday and Sunday.</p>\n</article>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } .card { background: #f8f9fa; padding: 1.5rem; border-radius: 8px; max-width: 320px; } .title { display: block; font-size: 1.25rem; color: steelblue; margin-bottom: 0.5rem; font-weight: normal; } p { margin: 0; color: #555; line-height: 1.5; }",
"id": "typography-3",
"title": "Font Weight & Style",
"description": "Apply weight and style variations like bold, light, italic to emphasize text.",
"task": "Make the paragraph '.emphasis' italic and bold using <code>font-style</code> and <code>font-weight</code>.",
"previewHTML": "<p class=\"emphasis\">This text should stand out.</p>",
"previewBaseCSS": "body { padding: 1rem; }",
"sandboxCSS": "",
"codePrefix": ".title {\n ",
"codePrefix": "/* Emphasize text */\n.emphasis {",
"initialCode": "",
"codeSuffix": "\n}",
"codeSuffix": "}",
"previewContainer": "preview-area",
"solution": "font-weight: bold;",
"concept": {
"explanation": "Font files contain multiple variations (weights and styles) of the same typeface. When you set font-weight: bold, the browser looks for a bold variant in the font family; if unavailable, it synthesizes boldness by artificially thickening letter strokes (called \"faux bold\"). Font-style: italic requests the true italic variant—if missing, browsers slant the regular font (\"oblique\" or \"faux italic\"). True variants look better because type designers craft them with proper letter spacing and stroke contrast, while synthesized versions simply distort the regular font mathematically.",
"diagram": "Font Variations Within a Font Family\n\nFont Family: 'Georgia'\n\n┌──────────────────────────────┐\n│ Georgia-Regular.ttf │ font-weight: 400 (normal)\n│ Georgia-Italic.ttf │ font-style: italic\n│ Georgia-Bold.ttf │ font-weight: 700 (bold)\n│ Georgia-BoldItalic.ttf │ both combined\n└──────────────────────────────┘\n\nFont-weight numeric scale:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n100 Thin (rarely used)\n300 Light\n400 Normal/Regular (default)\n600 Semi-Bold\n700 Bold (keyword: bold)\n900 Black/Heavy\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nTrue vs Synthetic (Faux):\n\nTrue Italic: Custom letterforms\nFaux Italic: Slanted regular ⚠️\n\nTrue Bold: Designed thick strokes\nFaux Bold: Artificially thickened ⚠️"
},
"validations": [
{ "type": "contains", "value": "font-style", "message": "Use <kbd>font-style</kbd> property", "options": { "caseSensitive": false } },
{
"type": "property_value",
"value": { "property": "font-style", "expected": "italic" },
"message": "Set font-style to <kbd>italic</kbd>"
},
{
"type": "contains",
"value": "font-weight",
"message": "Use <kbd>font-weight</kbd> property",
"options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "font-weight", "expected": "bold" },
@@ -74,75 +109,33 @@
]
},
{
"id": "text-transform",
"title": "Uppercase Labels",
"description": "The <kbd>text-transform</kbd> property changes text capitalization without editing the HTML. This keeps content semantic while styling it differently:<br><br>• <kbd>uppercase</kbd> — ALL CAPS (great for labels, buttons)<br>• <kbd>lowercase</kbd> — all lowercase<br>• <kbd>capitalize</kbd> — First Letter Of Each Word<br><br>Combined with <kbd>letter-spacing</kbd>, uppercase text creates a clean, modern label style often used in UI design.",
"task": "Style the tag labels. Add <kbd>text-transform: uppercase</kbd> and <kbd>letter-spacing: 1px</kbd>.",
"previewHTML": "<article>\n <div class=\"tags\">\n <span class=\"tag\">Design</span>\n <span class=\"tag\">CSS</span>\n <span class=\"tag\">Tutorial</span>\n </div>\n <h3>Getting Started with Typography</h3>\n <p>Learn the fundamentals of web typography in this beginner-friendly guide.</p>\n</article>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } .tags { display: flex; gap: 0.5rem; margin-bottom: 1rem; } .tag { background: #e8f4f8; color: steelblue; padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.75rem; } h3 { margin: 0 0 0.5rem; } p { margin: 0; color: #555; line-height: 1.5; }",
"id": "typography-4",
"title": "Text Decoration & Shadow",
"description": "Add decorative underlines, overlines, line-throughs and subtle shadows to text.",
"task": "Apply an underline with <code>text-decoration</code> and a light shadow using <code>text-shadow</code> on '.fancy'.",
"previewHTML": "<p class=\"fancy\">Fancy text effect!</p>",
"previewBaseCSS": "body { padding: 1rem; } .fancy { font-size: 1.25rem; }",
"sandboxCSS": "",
"codePrefix": ".tag {\n ",
"codePrefix": "/* Decorate text */\n.fancy {",
"initialCode": "",
"codeSuffix": "\n}",
"codeSuffix": "}",
"previewContainer": "preview-area",
"solution": "text-transform: uppercase;\n letter-spacing: 1px;",
"concept": {
"explanation": "Text-decoration draws lines relative to the text baseline using the browser's rendering engine—underlines sit below the baseline, overlines above the cap height, and line-throughs at the middle of the x-height. Text-shadow creates depth by rendering duplicate copies of text offset by X and Y coordinates, with optional blur radius and color. The browser draws shadows behind the original text using a Gaussian blur algorithm. Multiple shadows can be stacked (comma-separated) to create complex effects like glows, outlines, or 3D text, with each shadow rendered in order from bottom to top.",
"diagram": "Text Decoration & Shadow Rendering\n\ntext-decoration: underline;\n\n Cap Height ─┬─ Overline position\n │\n Mean Line ──┼─ Strike-through\n │\n Baseline ───┼─ Text sits here\n │\n └─ Underline position\n\ntext-shadow: 2px 2px 4px gray;\n │ │ │ └─ Color\n │ │ └───── Blur radius\n │ └───────── Vertical offset\n └───────────── Horizontal offset\n\nShadow rendering layers:\n\n┌──────────────────────────┐\n│ Original text (on top) │ ← Layer 3\n├──────────────────────────┤\n│ ░░Blurred shadow copy░░ │ ← Layer 2\n├──────────────────────────┤\n│ Background │ ← Layer 1\n└──────────────────────────┘\n\nCommon patterns:\n━━━━━━━━━━━━━━━━━━━━━━━━━\nSubtle depth: 1px 1px 2px rgba(0,0,0,0.3)\nGlow effect: 0 0 10px gold\n3D text: 2px 2px 0 black (no blur)"
},
"validations": [
{
"type": "property_value",
"value": { "property": "text-transform", "expected": "uppercase" },
"message": "Set text-transform to <kbd>uppercase</kbd>"
"type": "contains",
"value": "text-decoration",
"message": "Use <kbd>text-decoration</kbd> property",
"options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "letter-spacing", "expected": "1px" },
"message": "Set letter-spacing to <kbd>1px</kbd>"
}
]
},
{
"id": "text-decoration",
"title": "Text Decoration",
"description": "The <kbd>text-decoration</kbd> property adds lines to text. Common values:<br><br>• <kbd>underline</kbd> — line below text<br>• <kbd>line-through</kbd> — strikethrough<br>• <kbd>none</kbd> — removes decoration (useful for links)<br><br>You can also style decorations with <kbd>text-decoration-color</kbd> and <kbd>text-decoration-style</kbd>.",
"task": "Show the old price with a strikethrough. Add <kbd>text-decoration: line-through</kbd>.",
"previewHTML": "<div class=\"price-box\"><span class=\"old-price\">$49.99</span><span class=\"new-price\">$29.99</span></div>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } .price-box { display: flex; gap: 1rem; align-items: center; } .old-price { color: #999; font-size: 1rem; } .new-price { color: coral; font-size: 1.5rem; font-weight: bold; }",
"sandboxCSS": "",
"codePrefix": ".old-price {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"previewContainer": "preview-area",
"solution": "text-decoration: line-through;",
"validations": [
{
"type": "property_value",
"value": { "property": "text-decoration", "expected": "line-through" },
"message": "Set text-decoration to <kbd>line-through</kbd>"
}
]
},
{
"id": "text-shadow",
"title": "Text Shadow",
"description": "The <kbd>text-shadow</kbd> property adds shadow effects to text. The syntax is:<br><br><pre>text-shadow: x-offset y-offset blur color;</pre><br>Example: <kbd>text-shadow: 2px 2px 4px gray</kbd> creates a soft shadow offset down and right.",
"task": "Add depth to the heading with <kbd>text-shadow: 2px 2px 4px gray</kbd>.",
"previewHTML": "<h1 class=\"hero-title\">Welcome</h1>",
"previewBaseCSS": "body { font-family: system-ui; padding: 2rem; background: linear-gradient(135deg, #667eea, #764ba2); } .hero-title { margin: 0; font-size: 3rem; color: white; text-align: center; }",
"sandboxCSS": "",
"codePrefix": ".hero-title {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"previewContainer": "preview-area",
"solution": "text-shadow: 2px 2px 4px gray;",
"validations": [
{
"type": "contains",
"value": "text-shadow",
"message": "Use <kbd>text-shadow</kbd> property"
},
{
"type": "contains",
"value": "2px 2px",
"message": "Set offset to <kbd>2px 2px</kbd>"
"message": "Use <kbd>text-shadow</kbd> property",
"options": { "caseSensitive": false }
}
]
}

View File

@@ -7,94 +7,125 @@
"lessons": [
{
"id": "units-1",
"title": "Relative Units",
"description": "CSS offers two types of units: <em>absolute</em> (like <kbd>px</kbd>) and <em>relative</em> (like <kbd>%</kbd> and <kbd>rem</kbd>). Relative units adapt to their context, making layouts flexible and accessible.<br><br><strong>Common relative units:</strong><br>• <kbd>%</kbd> Relative to parent element<br>• <kbd>rem</kbd> Relative to root font size (typically 16px)<br>• <kbd>em</kbd> Relative to element's font size<br><br>A common pattern for readable content: set <kbd>width: 100%</kbd> so it fills available space, then <kbd>max-width: 40rem</kbd> to cap line length for readability.",
"task": "This article text runs too wide on large screens. Add <kbd>max-width: 40rem</kbd> to <kbd>.article</kbd> for optimal reading width.",
"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; }",
"title": "Absolute vs. Relative Units",
"description": "Learn the difference between px, rem, em, %, and vw/vh for flexible, responsive layouts.<br><br><pre>width: 80%; /* relative to parent */\nmax-width: 40rem; /* relative to root font */\npadding: 16px; /* fixed pixels */</pre>",
"task": "Set the <kbd>width</kbd> of <kbd>.box</kbd> to <kbd>80%</kbd> and <kbd>max-width</kbd> to <kbd>40rem</kbd>.",
"previewHTML": "<div class=\"box\">Resize me!</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { background: #f5f5f5; padding: 1rem; }",
"sandboxCSS": "",
"codePrefix": ".article {\n ",
"codePrefix": "/* Set flexible sizing */\n.box {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "max-width: 40rem;",
"codeSuffix": "}",
"solution": " width: 80%;\n max-width: 40rem;",
"previewContainer": "preview-area",
"concept": {
"explanation": "CSS units fall into two categories: absolute units (px) with fixed sizes, and relative units (%, rem, em, vw, vh) that scale based on context. Absolute units like 16px always render at the same physical size regardless of user settings, while relative units adapt to user preferences and screen sizes. Rem (root em) is preferred for most spacing because 1rem equals the root font size (usually 16px), so if a user increases their browser's font size for accessibility, all rem-based spacing scales proportionally. Percentage units (%) are relative to the parent element's size, making them perfect for fluid layouts that adapt to container width.",
"diagram": "Unit Types & How They Calculate\n\nAbsolute Units (Fixed Size):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━\npx (pixels) → 16px always = 16 pixels\n (ignores user font settings)\n\nRelative Units (Scale with Context):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nrem (root em) → 1rem = root font-size\n If html { font-size: 16px }\n then 2rem = 32px\n Scales with user settings ✓\n\n% (percent) → 80% = 80% of parent width\n Parent: 500px → 80% = 400px\n Parent: 800px → 80% = 640px\n Fluid layouts ✓\n\nem (element) → 1em = current font-size\n .box { font-size: 20px }\n padding: 1em = 20px\n Compounds in nested elements ⚠️\n\nvw/vh → 50vw = 50% viewport width\n 1vh = 1% viewport height\n\nWhy rem is preferred:\n┌──────────────────────────────────┐\n│ User increases browser font size │\n│ (Settings → Appearance → Text) │\n└────────────┬─────────────────────┘\n ↓\n┌──────────────────────────────────┐\n│ rem-based spacing scales up ✓ │\n│ px-based spacing stays fixed ✗ │\n└──────────────────────────────────┘\nAccessibility & responsive design!"
},
"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": "40rem" },
"message": "Set <kbd>max-width: 40rem</kbd>"
"message": "Set max-width to <kbd>40rem</kbd>"
}
]
},
{
"id": "units-2",
"title": "CSS Variables",
"description": "CSS custom properties (variables) let you define reusable values. Define them with <kbd>--name</kbd> and use them with <kbd>var(--name)</kbd>. Variables defined on <kbd>:root</kbd> are available everywhere.",
"task": "Define <kbd>--brand: steelblue</kbd> in <kbd>:root</kbd>, then use it as the <kbd>background</kbd> color for <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; }",
"title": "CSS Custom Properties",
"description": "Define and reuse variables (--custom properties) to centralize your theme values.<br><br><pre>:root {\n --main-color: mediumpurple;\n}\n.themed {\n border-color: var(--main-color);\n}</pre>",
"task": "Create a <kbd>--main-color</kbd> variable in <kbd>:root</kbd> with <kbd>mediumpurple</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; }",
"sandboxCSS": "",
"codePrefix": ":root {\n ",
"codePrefix": "/* Define and use a CSS variable */\n:root {",
"initialCode": "",
"codeSuffix": "\n}\n\n.btn {\n background: var(--brand);\n}",
"solution": "--brand: steelblue;",
"codeSuffix": "}\n.themed { }",
"solution": " --main-color: mediumpurple;\n}\n.themed {\n border-color: var(--main-color);",
"previewContainer": "preview-area",
"concept": {
"explanation": "CSS custom properties (variables) are values you define once and reference throughout your stylesheet using the var() function. They follow the CSS cascade, meaning variables defined on :root (the html element) are inherited by all descendants, while variables defined on specific elements are scoped to those elements and their children. When you change a custom property value, all references to that variable automatically update, making theme management and design systems much easier to maintain. Unlike preprocessor variables (Sass/Less), CSS custom properties are live in the browser and can be updated dynamically with JavaScript or media queries.",
"diagram": "CSS Custom Properties & Inheritance\n\nDefinition & Reference:\n\n:root {\n --main-color: mediumpurple; ← Define\n --spacing: 1rem;\n}\n\n.themed {\n border-color: var(--main-color); ← Reference\n padding: var(--spacing);\n}\n\nInheritance cascade:\n\n┌─────────────────────────────┐\n│ :root (html element) │\n│ --primary: blue │ ← Defined here\n└──────────┬──────────────────┘\n │ Inherits ↓\n ┌──────┴──────┐\n │ body │\n │ (inherits) │\n └──────┬──────┘\n │ Inherits ↓\n ┌──────┴──────────┐\n │ .card │\n │ color: var(--primary) │ ← Can use it!\n └─────────────────┘\n\nScoping example:\n\n:root { --theme: light; }\n\n.dark-mode {\n --theme: dark; ← Overrides for this\n} subtree only\n\nDynamic updates:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nCSS: :root { --size: 16px; }\n ↓\nJS: document.documentElement\n .style.setProperty('--size', '20px');\n ↓\nAll var(--size) references update! ✓\n\nVs Preprocessor Variables:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nSass: $color: blue; ← Compile-time\nCSS: --color: blue; ← Runtime (live)"
},
"validations": [
{
"type": "contains",
"value": "--brand",
"message": "Define <kbd>--brand</kbd> variable",
"value": "--main-color",
"message": "Define <kbd>--main-color</kbd> in :root",
"options": { "caseSensitive": false }
},
{
"type": "contains",
"value": "steelblue",
"message": "Set the value to <kbd>steelblue</kbd>",
"value": "var(--main-color)",
"message": "Use <kbd>var(--main-color)</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": "calc() Function",
"description": "The <kbd>calc()</kbd> function lets you mix different units in calculations. This is essential for layouts that combine fixed and flexible sizing, like a sidebar layout.",
"task": "The main content should fill the remaining space after the 200px sidebar. Set <kbd>width: calc(100% - 200px)</kbd> on <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; }",
"title": "Unit Calculations (calc)",
"description": "Use the <kbd>calc()</kbd> function to combine different units in one expression.<br><br><pre>width: calc(100% - 2rem);\nmin-height: calc(10vh + 1rem);</pre>",
"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; }",
"sandboxCSS": "",
"codePrefix": ".main {\n ",
"codePrefix": "/* Use calc for dynamic sizing */\n.sized {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "width: calc(100% - 200px);",
"codeSuffix": "}",
"solution": " width: calc(100% - 2rem);\n min-height: calc(10vh + 1rem);",
"previewContainer": "preview-area",
"concept": {
"explanation": "The calc() function performs mathematical calculations at runtime, allowing you to mix different unit types (px, %, rem, vw, etc.) in a single expression. The browser evaluates calc() expressions during layout calculation, after all relative units have been resolved to their pixel values, then performs the arithmetic operation. This is powerful for responsive layouts because you can combine fluid units (%) with fixed spacing (rem/px), like calc(100% - 2rem) for \"full width minus some padding.\" Spaces around + and - operators are required because calc(10vh+1rem) would be parsed as a single invalid unit, while calc(10vh + 1rem) correctly separates the operands.",
"diagram": "How calc() Works at Runtime\n\nExpression: width: calc(100% - 2rem);\n\nStep 1: Resolve relative units\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nParent width: 500px\n100% → 500px\n\nRoot font-size: 16px\n2rem → 32px (2 × 16)\n\nStep 2: Perform calculation\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━\ncalc(100% - 2rem)\n → calc(500px - 32px)\n → 468px ✓\n\nVisual representation:\n\n┌──────────────────────────────┐\n│ Parent container (500px) │\n│ ┌──────────────────────────┐ │\n│ │ .sized (468px) │ │ ← calc(100% - 2rem)\n│ │ │ │\n│ └──────────────────────────┘ │\n│ ◀── 16px gap (1rem) on each │\n│ side = 32px total │\n└──────────────────────────────┘\n\nMixing units:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\ncalc(100% - 2rem) Fluid - Fixed\ncalc(50vw + 100px) Viewport + Fixed\ncalc(2rem * 3) Multiplication\ncalc(100% / 3) Division\n\nImportant syntax rules:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\ncalc(10vh + 1rem) ✓ Spaces around +/-\ncalc(10vh+1rem) ✗ Treated as one unit\ncalc(2rem * 3) ✓ No space needed for */\ncalc(100%/3) ✓ Division works too"
},
"validations": [
{ "type": "contains", "value": "calc", "message": "Use <kbd>calc()</kbd> function", "options": { "caseSensitive": false } },
{
"type": "regex",
"value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
"message": "Set <kbd>width: calc(100% - 200px)</kbd>",
"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>",
"options": { "caseSensitive": false }
}
]
},
{
"id": "units-4",
"title": "Viewport Units",
"description": "Viewport units size elements relative to the browser window:<br>• <kbd>vw</kbd> 1% of viewport width<br>• <kbd>vh</kbd> 1% of viewport height<br><br>These are perfect for full-screen sections like hero banners.",
"task": "Make this hero section fill the viewport height by setting <kbd>min-height: 100vh</kbd> on <kbd>.hero</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; }",
"title": "Viewport & Responsive Units",
"description": "Control layouts relative to viewport size with vw, vh, and vmin/vmax units.<br><br><pre>width: 50vw; /* 50% of viewport width */\nheight: 20vh; /* 20% of viewport height */</pre>",
"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; }",
"sandboxCSS": "",
"codePrefix": ".hero {\n ",
"codePrefix": "/* Use viewport units */\n.view {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "min-height: 100vh;",
"codeSuffix": "}",
"solution": " width: 50vw;\n height: 20vh;",
"previewContainer": "preview-area",
"concept": {
"explanation": "Viewport units (vw, vh, vmin, vmax) are relative to the browser window dimensions, not the parent element. 1vw equals 1% of the viewport width, and 1vh equals 1% of the viewport height, so 50vw is always half the screen width regardless of element nesting. These units are perfect for full-screen hero sections, responsive typography, and layouts that scale with screen size. The browser recalculates viewport unit values when the window is resized, making elements automatically adapt without media queries. Vmin uses the smaller dimension (min of width/height) while vmax uses the larger, useful for ensuring elements fit on both portrait and landscape orientations.",
"diagram": "Viewport Units & How They Calculate\n\nViewport Dimensions:\n┌────────────────────────────────┐ ↕\n│ │ 800px\n│ Browser Window │ viewport\n│ (Viewport) │ height\n│ │ ↕\n│ │\n└────────────────────────────────┘\n◀────── 1400px viewport width ──▶\n\nUnit calculations:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n1vw = 1400px ÷ 100 = 14px\n1vh = 800px ÷ 100 = 8px\n\n50vw = 50 × 14px = 700px (half width)\n20vh = 20 × 8px = 160px (1/5 height)\n\nvmin = min(1vw, 1vh) = min(14px, 8px) = 8px\nvmax = max(1vw, 1vh) = max(14px, 8px) = 14px\n\nViewport vs Percentage:\n\n% units (relative to parent):\n┌─────────────────────────────┐\n│ Parent (600px) │\n│ ┌─────────────────────┐ │\n│ │ width: 50% │ │ ← 300px\n│ │ (50% of parent) │ │ (50% of 600px)\n│ └─────────────────────┘ │\n└─────────────────────────────┘\n\nvw units (relative to viewport):\n┌────────────────────────────────┐ Viewport (1400px)\n│ ┌──────────────────────┐ │\n│ │ Parent (600px) │ │\n│ │ ┌──────────────────┐ │ │\n│ │ │ width: 50vw │ │ │ ← 700px\n│ │ │ (50% of viewport)│ │ │ (50% of 1400px)\n│ │ └──────────────────┘ │ │ Escapes parent!\n│ └──────────────────────┘ │\n└────────────────────────────────┘\n\nCommon use cases:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nFull-screen: width: 100vw; height: 100vh;\nHero section: min-height: 80vh;\nResponsive: font-size: calc(1rem + 1vw);\nSquare ratio: width: 50vmin; height: 50vmin;"
},
"validations": [
{
"type": "property_value",
"value": { "property": "min-height", "expected": "100vh" },
"message": "Set <kbd>min-height: 100vh</kbd>"
}
{ "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>" }
]
}
]

View File

@@ -18,6 +18,10 @@
"codeSuffix": "}",
"solution": " transition: background-color 0.3s;",
"previewContainer": "preview-area",
"concept": {
"explanation": "CSS transitions interpolate (calculate in-between values) smoothly between a property's start and end values over a specified duration. When you hover the button, the browser detects the background-color change and automatically generates intermediate color values at each frame (typically 60 frames per second). For colors, the browser converts both values to RGB, then calculates proportional changes for each channel—for example, at 50% through a 0.3s transition, the color is halfway between black and white, resulting in gray.",
"diagram": "How CSS Transitions Interpolate Values\n\nTransition: background-color 0.3s\nStart: black → End: white\n\nTime progression (60fps = 60 frames in 0.3s):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n0.00s (0%) rgb(0, 0, 0) ███████ black\n ↓ interpolate\n0.05s (17%) rgb(43, 43, 43) ███████ dark gray\n ↓ interpolate\n0.15s (50%) rgb(128,128,128) ███████ gray\n ↓ interpolate\n0.25s (83%) rgb(212,212,212) ███████ light gray\n ↓ interpolate\n0.30s (100%) rgb(255,255,255) ███████ white\n\nRGB interpolation formula at time t:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nvalue = start + (end - start) × progress\n\nAt t=0.15s (50% through 0.3s duration):\nprogress = 0.15 / 0.3 = 0.5\n\nRed: 0 + (255 - 0) × 0.5 = 128\nGreen: 0 + (255 - 0) × 0.5 = 128\nBlue: 0 + (255 - 0) × 0.5 = 128\n\nResult: rgb(128, 128, 128) ✓\n\nBrowser rendering process:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n1. Detect property change (hover triggers)\n2. Start transition timer (0.3s duration)\n3. Calculate frame count (0.3s × 60fps = 18 frames)\n4. For each frame:\n - Calculate progress (elapsed / duration)\n - Interpolate RGB values\n - Repaint element\n5. End at final value\n\nTransitionable properties:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nColors: rgb values interpolate\nLengths: px, rem, % interpolate \nTransforms: matrix values interpolate\nOpacity: 0-1 range interpolates\nNOT text: \"foo\" → \"bar\" can't interpolate!"
},
"validations": [
{
"type": "contains",
@@ -46,6 +50,10 @@
"codeSuffix": "}",
"solution": " transition-timing-function: ease-in-out;",
"previewContainer": "preview-area",
"concept": {
"explanation": "Timing functions (also called easing functions) control the rate of change during a transition by applying a mathematical curve to the linear progress over time. Instead of changing at a constant speed (linear), timing functions accelerate or decelerate at different points, making animations feel more natural. For example, ease-in-out starts slow, speeds up in the middle, then slows down at the end, mimicking real-world physics where objects don't instantly reach full speed or stop abruptly.",
"diagram": "Timing Functions & Animation Pacing\n\nLinear progress over time:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nTime: 0% 25% 50% 75% 100%\nProgress: 0% 25% 50% 75% 100%\nSpeed: ═══════════════════════════ constant\n\nEase-in (accelerate):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nTime: 0% 25% 50% 75% 100%\nProgress: 0% 6% 25% 56% 100%\nSpeed: ─────────────────────────▶ speeds up\n slow fast\n\nEase-out (decelerate):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nTime: 0% 25% 50% 75% 100%\nProgress: 0% 44% 75% 94% 100%\nSpeed: ◀───────────────────────── slows down\n fast slow\n\nEase-in-out (accelerate then decelerate):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nTime: 0% 25% 50% 75% 100%\nProgress: 0% 10% 50% 90% 100%\nSpeed: ─────▶━━━━━━◀───── natural motion\n slow fast slow\n\nBézier curve visualization:\n\n 1 ┤ ╭──── ease-out\n │ ╭───╯ (fast start)\n │ ╭────╯\n0.5 ┤ ╭────╯──── ease-in-out\n │ ╭───╯ (smooth)\n │ ╭───╯\n 0 ┤──╯─────────────────── linear\n └──┬────┬────┬────┬──── ease-in\n 0 0.25 0.5 0.75 1 (slow start)\n Time →\n\nCommon timing functions:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nlinear cubic-bezier(0, 0, 1, 1)\nease cubic-bezier(0.25, 0.1, 0.25, 1) [default]\nease-in cubic-bezier(0.42, 0, 1, 1)\nease-out cubic-bezier(0, 0, 0.58, 1)\nease-in-out cubic-bezier(0.42, 0, 0.58, 1)\n\nReal-world analogy:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nCar accelerating from stop sign:\n ease-in → Pressing gas pedal gradually\n\nCar approaching red light:\n ease-out → Braking smoothly to stop\n\nCar between two stop signs:\n ease-in-out → Accelerate, cruise, brake"
},
"validations": [
{
"type": "contains",
@@ -73,6 +81,10 @@
"codeSuffix": "}\n.ball { }",
"solution": " 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;",
"previewContainer": "preview-area",
"concept": {
"explanation": "Keyframe animations define multiple snapshots (keyframes) of property values at specific percentages (0% is start, 100% is end, 50% is halfway), and the browser interpolates smoothly between them. Unlike transitions which only animate from one state to another, keyframes let you define complex multi-step animations with precise control—the browser automatically calculates timing between each keyframe. The 'infinite' keyword makes the animation loop continuously, restarting from 0% each time it completes.",
"diagram": "Keyframe Animation Timeline & Interpolation\n\n@keyframes bounce {\n 0% { transform: translateY(0px); } ← implicit start\n 50% { transform: translateY(-20px); } ← explicit midpoint\n 100% { transform: translateY(0px); } ← implicit end\n}\n\nanimation: bounce 1s infinite;\n\nTimeline breakdown (1 second duration):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n0.0s (0%) ●─────────────────── translateY(0px)\n │ interpolate [starting position]\n │ over 0.5s\n ↓\n0.5s (50%) ●─────────────────── translateY(-20px)\n │ interpolate [peak - 20px up]\n │ over 0.5s\n ↓\n1.0s (100%) ●─────────────────── translateY(0px)\n ↓ infinite loop [back to start]\n0.0s restart ●\n\nVisual representation:\n\n -20px ↑ ● ← 50% keyframe (peak)\n │ ╲\n │╱ ╲\n 0px ●─────● ← 0% and 100% keyframes\n ↑ ↑\n 0s 1s\n\nInterpolation between keyframes:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nFrom 0% to 50% (0s to 0.5s):\nStart: translateY(0px)\nEnd: translateY(-20px)\n\nAt 0.25s (halfway between 0% and 50%):\nprogress = 0.25 / 0.5 = 0.5\nvalue = 0 + (-20 - 0) × 0.5 = -10px\nResult: translateY(-10px) ✓\n\nFrom 50% to 100% (0.5s to 1s):\nStart: translateY(-20px)\nEnd: translateY(0px)\n\nAt 0.75s (halfway between 50% and 100%):\nprogress = (0.75 - 0.5) / 0.5 = 0.5\nvalue = -20 + (0 - (-20)) × 0.5 = -10px\nResult: translateY(-10px) ✓\n\nKeyframes vs Transitions:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nTransitions:\n A → B (one state change)\n Triggered by hover/focus/class change\n Example: button:hover { color: red; }\n\nKeyframes:\n A → B → C → D... (multiple states)\n Runs automatically when element exists\n Example: loading spinner, bounce effect\n Can loop infinitely\n\nImplicit keyframes:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nIf you don't define 0% or 100%, browser uses\ncurrent computed values:\n\n@keyframes bounce {\n 50% { transform: translateY(-20px); }\n}\n↓ Browser expands to:\n@keyframes bounce {\n 0% { transform: translateY(0px); } ← added\n 50% { transform: translateY(-20px); }\n 100% { transform: translateY(0px); } ← added\n}"
},
"validations": [
{
"type": "contains",
@@ -113,6 +125,10 @@
"codeSuffix": "}",
"solution": " animation-name: pulse;\n animation-duration: 2s;\n animation-delay: 1s;\n animation-iteration-count: 2;\n animation-fill-mode: forwards;",
"previewContainer": "preview-area",
"concept": {
"explanation": "Animation properties give you precise control over playback timing and behavior. Animation-delay postpones the animation start (useful for choreographing sequences), while animation-iteration-count determines how many times it repeats (1, 2, or 'infinite'). Animation-fill-mode controls styles before/after playback: 'none' removes animation styles when not playing, 'forwards' keeps the final keyframe styles after completion, 'backwards' applies starting styles during delay, and 'both' combines both behaviors. These properties control exactly when animations start, how long they run, and what happens before and after playback.",
"diagram": "Animation Properties & Timeline Control\n\nanimation-name: pulse;\nanimation-duration: 2s;\nanimation-delay: 1s;\nanimation-iteration-count: 2;\nanimation-fill-mode: forwards;\n\nComplete timeline:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n0s 1s 3s 5s\n│───────────│───────────│───────────│\n│ DELAY │ ITERATION │ ITERATION │ END\n│ (wait) │ #1 │ #2 │ (hold)\n│ │ │ │\n│ ●●●●●●● │ ▶────────▶│ ▶────────▶│ ████\n│ waiting │ playing │ playing │ frozen\n│ │ (2s) │ (2s) │ at\n│ │ │ │ 100%\n\nElement state at each phase:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nBefore delay (0s - 1s):\n background: black ← original CSS\n (animation hasn't started yet)\n\nDuring iteration 1 (1s - 3s):\n 0%: background: black\n 50%: background: white\n 100%: background: limegreen\n (animating through keyframes)\n\nDuring iteration 2 (3s - 5s):\n Repeats: black → white → limegreen\n (second playthrough)\n\nAfter animation (5s+):\n background: limegreen ← fill-mode: forwards\n (stuck at 100% keyframe)\n\nanimation-fill-mode explained:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nnone (default):\n Before: original CSS ●●●●●●●\n During: animation ▶────▶\n After: original CSS ●●●●●●●\n\nforwards:\n Before: original CSS ●●●●●●●\n During: animation ▶────▶\n After: 100% keyframe ████████ ← stays!\n\nbackwards:\n Before: 0% keyframe ████████ ← applies!\n During: animation ▶────▶\n After: original CSS ●●●●●●●\n\nboth:\n Before: 0% keyframe ████████ ← applies!\n During: animation ▶────▶\n After: 100% keyframe ████████ ← stays!\n\nanimation-iteration-count:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n1 ▶────▶ (play once)\n2 ▶────▶ ▶────▶ (play twice)\n3 ▶────▶ ▶────▶ ▶────▶\ninfinite ▶────▶ ▶────▶ ▶────▶... (loop forever)\n2.5 ▶────▶ ▶────▶ ▶── (2.5 times)\n\nanimation-delay use cases:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nStaggered animations (cascade effect):\n .item:nth-child(1) { animation-delay: 0s; }\n .item:nth-child(2) { animation-delay: 0.1s; }\n .item:nth-child(3) { animation-delay: 0.2s; }\n\n Result: items animate one after another ↓\n\nNegative delay (start mid-animation):\n animation-delay: -1s; ← starts 1s into animation\n Useful for randomizing loop positions\n\nShorthand syntax:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nanimation: pulse 2s 1s 2 forwards;\n │ │ │ │ └─ fill-mode\n │ │ │ └──── iteration-count\n │ │ └─────── delay\n │ └────────── duration\n └──────────────── name"
},
"validations": [
{
"type": "property_value",

View File

@@ -17,6 +17,11 @@
"initialCode": "",
"codeSuffix": "}",
"previewContainer": "preview-area",
"concept": {
"explanation": "Flexbox is a one-dimensional layout system designed for distributing space and aligning items along a single axis (row or column). When you set display: flex on a container, it establishes two perpendicular axes: the main axis (horizontal by default, left to right) and the cross axis (vertical, top to bottom). The justify-content property controls alignment along the main axis (horizontal centering in this case), while align-items controls alignment along the cross axis (vertical centering). This separation of concerns makes Flexbox perfect for one-dimensional layouts like navigation bars, card rows, or button groups where you need precise control over spacing and alignment.",
"diagram": "Flexbox: One-Dimensional Layout System\n\nFlexbox Container with Main & Cross Axes:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n justify-content: center\n (main axis alignment)\n ↓\n ┌─────────────────────────┐\n │ ┌───┐ │ ← align-items: center\n │ ┌───┤ 2 ├───┐ │ (cross axis alignment)\n │ │ 1 └───┘ 3 │ │\n │ └───────────┘ │\n └─────────────────────────┘\n ← main axis →\n\nDefault Flexbox Behavior:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nWithout centering:\ndisplay: flex\n ┌─────────────────────────┐\n │┌───┬───┬───┐ │\n ││ 1 │ 2 │ 3 │ │\n │└───┴───┴───┘ │\n └─────────────────────────┘\n ↑ Items flow left-to-right\n along main axis by default\n\nWith justify-content: center:\n ┌─────────────────────────┐\n │ ┌───┬───┬───┐ │\n │ │ 1 │ 2 │ 3 │ │\n │ └───┴───┴───┘ │\n └─────────────────────────┘\n ↑ Centered horizontally\n\nWith align-items: center:\n ┌─────────────────────────┐\n │ ┌───┬───┬───┐ │\n │ │ 1 │ 2 │ 3 │ │\n │ └───┴───┴───┘ │\n └─────────────────────────┘\n ↑ Centered vertically\n\nMain Axis vs Cross Axis:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nRow direction (default):\n main axis: horizontal →\n cross axis: vertical ↓\n\nColumn direction:\n main axis: vertical ↓\n cross axis: horizontal →",
"containerVsItem": "display: flex, justify-content, and align-items are CONTAINER properties set on the parent element. They control the layout and alignment of all child items collectively. Individual items can override cross-axis alignment with align-self."
},
"validations": [
{ "type": "contains", "value": "display", "message": "Use <kbd>display: flex</kbd>", "options": { "caseSensitive": false } },
{
@@ -40,6 +45,11 @@
"initialCode": "",
"codeSuffix": "}\n.item { }",
"previewContainer": "preview-area",
"concept": {
"explanation": "The flex shorthand property combines three values: flex-grow (how much an item grows to fill available space), flex-shrink (how much it shrinks when space is limited), and flex-basis (the initial size before growing or shrinking). The syntax flex: 1 1 100px means each item starts at 100px width, can grow to take up extra space (factor of 1), and can shrink below 100px if needed (factor of 1). When combined with flex-wrap: wrap, items that can't fit on one line wrap to the next, creating responsive multi-line layouts without media queries. This makes flex perfect for responsive card grids, tag lists, or any layout that needs to adapt fluidly to container width.",
"diagram": "Flex Shorthand: flex-grow flex-shrink flex-basis\n\nSyntax breakdown:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nflex: 1 1 100px\n ↓ ↓ ↓\n grow shrink basis (initial size)\n\nHow flex: 1 1 100px works:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nContainer: 400px wide\n3 items with flex: 1 1 100px\n\nStep 1: Start with basis (100px each)\n┌────────────────────────────────────┐\n│ [100px][100px][100px] ←100px │\n│ A B C extra space │\n└────────────────────────────────────┘\n 300px used, 100px remaining\n\nStep 2: Distribute extra space (flex-grow: 1)\nEach item grows by: 100px / 3 = 33.33px\n┌────────────────────────────────────┐\n│ [133px][133px][133px] │\n│ A B C │\n└────────────────────────────────────┘\n All items grow equally (same grow factor)\n\nWith flex-wrap: wrap\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nNarrow container (250px):\n3 items @ 100px each = 300px needed\n\nWithout wrap (overflow):\n┌──────────────────────┐\n│[83px][83px][83px]│ overflow\n│ A B C │\n└──────────────────────┘\n Items shrink to fit (flex-shrink: 1)\n\nWith wrap (responsive):\n┌──────────────────────┐\n│ [125px][125px] │ Line 1: 2 items\n│ A B │\n│ [125px] │ Line 2: 1 item\n│ C │\n└──────────────────────┘\n Items wrap to new line, grow to fill\n\nCommon flex patterns:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nflex: 1 → flex: 1 1 0 (equal widths)\nflex: auto → flex: 1 1 auto (content-based)\nflex: none → flex: 0 0 auto (fixed size)\nflex: 1 1 100px → grow, shrink, 100px base",
"containerVsItem": "flex-wrap is a CONTAINER property that controls whether items wrap to new lines. The flex shorthand (flex-grow, flex-shrink, flex-basis) is an ITEM property that controls how individual items grow, shrink, and their starting size."
},
"validations": [
{
"type": "contains",
@@ -67,6 +77,11 @@
"initialCode": "",
"codeSuffix": "}",
"previewContainer": "preview-area",
"concept": {
"explanation": "CSS Grid is a two-dimensional layout system that divides space into rows and columns, creating a matrix of cells where items can be placed. Unlike Flexbox (which handles one dimension at a time), Grid controls both dimensions simultaneously, making it ideal for page layouts, complex card arrangements, or any design that needs precise row and column alignment. The fr (fraction) unit divides available space proportionally—repeat(3, 1fr) creates three equal columns that each take 1/3 of the container width. The gap property adds spacing between grid items without affecting the outer edges, eliminating the need for complex margin calculations. Grid's ability to align content in both dimensions makes it the best choice for two-dimensional layouts.",
"diagram": "CSS Grid: Two-Dimensional Layout System\n\nGrid vs Flexbox:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nFlexbox (1D):\n┌────────────────────────┐\n│ [1] [2] [3] [4] │ One axis (row)\n└────────────────────────┘\n\nGrid (2D):\n┌───────┬───────┬───────┐\n│ [1] │ [2] │ [3] │ Rows AND\n├───────┼───────┼───────┤ Columns\n│ [4] │ │ │ simultaneously\n└───────┴───────┴───────┘\n\nGrid with repeat(3, 1fr) and gap: 1rem:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n 1fr 1fr 1fr\n (33%) (33%) (33%)\n ↓ ↓ ↓\n┌─────────┬─────────┬─────────┐\n│ 1 │ 2 │ 3 │ ← Row 1\n├─────────┼─────────┼─────────┤\n│ 4 │ │ │ ← Row 2 (auto)\n└─────────┴─────────┴─────────┘\n ↑ gap: 1rem between cells\n\nHow fr units work:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nContainer: 600px wide, gap: 20px (2 gaps)\nAvailable: 600px - 40px = 560px\n\nrepeat(3, 1fr) = 3 equal fractions\nEach column: 560px / 3 = 186.67px\n\n┌──186px──┬──186px──┬──186px──┐\n│ 1 │ 2 │ 3 │\n└─────────┴─────────┴─────────┘\n 20px gap 20px gap\n\nDifferent fr ratios:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\ngrid-template-columns: 1fr 2fr 1fr\n(1/4 + 2/4 + 1/4 = 4 parts total)\n\n┌──140px──┬───280px───┬──140px──┐\n│ 1fr │ 2fr │ 1fr │\n│ (25%) │ (50%) │ (25%) │\n└─────────┴───────────┴─────────┘\n\nWhen to use Grid vs Flexbox:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nGrid: Page layouts, card grids, forms\nFlexbox: Navigation bars, button groups,\n single-axis content flow",
"containerVsItem": "display: grid, grid-template-columns, and gap are CONTAINER properties that define the grid structure. Items automatically flow into grid cells in order, or can be explicitly placed using grid-column/grid-row (item properties)."
},
"validations": [
{ "type": "contains", "value": "display: grid", "message": "Use <kbd>display: grid</kbd>", "options": { "caseSensitive": false } },
{
@@ -96,6 +111,11 @@
"initialCode": "",
"codeSuffix": "}",
"previewContainer": "preview-area",
"concept": {
"explanation": "CSS Grid uses numbered lines (not cells) to position and span items across the grid. Grid lines run between columns and rows, starting at 1 on the left/top edge. The syntax grid-column: 1 / span 2 means 'start at line 1 and span across 2 column tracks,' effectively occupying the space from line 1 to line 3. The browser automatically flows remaining items around the spanning item, filling available cells. This line-based placement system allows for complex overlapping layouts, featured items that span multiple columns/rows, and precise control over where each element appears in the grid. Unlike absolute positioning, grid-placed items still participate in the document flow and respond to content changes.",
"diagram": "Grid Lines & Spanning Items\n\nGrid line numbering:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n 1 2 3 4 ← Column lines\n ↓ ↓ ↓ ↓\n1 → ┌───────┬───────┬───────┐\n │ │ │ │ Row 1\n2 → ├───────┼───────┼───────┤\n │ │ │ │ Row 2\n3 → └───────┴───────┴───────┘\n\nLines run BETWEEN cells, not through them!\n\nSpanning with grid-column: 1 / span 2\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n 1 2 3 4\n ↓ ↓ ↓ ↓\n1 → ┌───────────────┬───────┐\n │ Featured │ 2 │\n │ (spans 2) │ │\n2 → ├───────┬───────┼───────┤\n │ 3 │ 4 │ │\n3 → └───────┴───────┴───────┘\n ↑\n Starts at line 1\n Spans 2 column tracks (1→2→3)\n Ends at line 3\n\nSpan syntax variations:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\ngrid-column: 1 / span 2\n Start at line 1, span 2 tracks\n\ngrid-column: 1 / 3\n Start at line 1, end at line 3\n (Same result as above)\n\ngrid-column: span 2\n Auto-place and span 2 tracks\n\ngrid-row: 1 / span 2\n Span 2 rows vertically\n\nComplex spanning example:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n.header { grid-column: 1 / 4; } /* Full width */\n.sidebar { grid-row: 2 / 4; } /* 2 rows tall */\n\n 1 2 3 4\n ↓ ↓ ↓ ↓\n1 → ┌───────────────────────┐\n │ Header │ ← Spans 3 columns\n2 → ├───────┬───────────────┤\n │ Side │ Content │\n │ bar │ │ ← Sidebar spans\n3 → │ ↓ ├───────┬───────┤ 2 rows\n │ │ 3 │ 4 │\n4 → └───────┴───────┴───────┘\n\nWhy use line-based placement:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n✓ Create magazine-style layouts\n✓ Feature items spanning multiple cells\n✓ Overlap items intentionally (z-index works)\n✓ Precise control without absolute positioning\n✓ Items still flow with content changes",
"containerVsItem": "grid-column and grid-row are ITEM properties that control where individual items are placed and how many tracks they span. The grid container defines the track structure (grid-template-columns/rows), while items decide their own placement within that structure."
},
"validations": [
{
"type": "contains",

View File

@@ -18,6 +18,11 @@
"codeSuffix": "",
"solution": "@media (max-width: 600px) {\n .panel {\n background: lightcoral;\n }\n}",
"previewContainer": "preview-area",
"concept": {
"explanation": "Media queries are conditional CSS rules that the browser evaluates continuously as the viewport changes. When you write @media (max-width: 600px), the browser checks if the viewport width is 600 pixels or less—if true, it applies the enclosed styles; if false, it ignores them. The browser re-evaluates this condition on every resize, instantly applying or removing styles based on viewport size, making responsive design possible without JavaScript. Common media features include width, height, orientation (portrait/landscape), and prefers-color-scheme (light/dark mode).",
"diagram": "Media Query Evaluation Process\n\nHow @media (max-width: 600px) works:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nBrowser checks viewport width continuously:\n\nViewport: 800px wide\n┌─────────────────────────────────┐\n│ @media (max-width: 600px) │\n│ Is 800px ≤ 600px? │\n│ NO → Styles NOT applied │\n│ │\n│ .panel { background: lightblue; }\n└─────────────────────────────────┘\n (default style)\n\nUser resizes window → 500px wide\n┌────────────────────────┐\n│ @media (max-width: 600px) │\n│ Is 500px ≤ 600px? │\n│ YES → Styles applied │\n│ │\n│ .panel { background: lightcoral; }\n└────────────────────────┘\n (media query style wins)\n\nBreakpoint Behavior:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n 0px 600px ∞\n ├──────────────────┼─────────────────►\n lightcoral │ lightblue\n (max-width) │ (default)\n ↑\n breakpoint\n (600px)\n\nCascade with Media Queries:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nCSS source order:\n.panel {\n background: lightblue; /* 1. Base style */\n}\n\n@media (max-width: 600px) {\n .panel {\n background: lightcoral; /* 2. Override when\n } condition matches */\n}\n\nWhen viewport ≤ 600px:\n Both rules have same specificity (0,0,1,0)\n Media query comes later → wins cascade\n Result: lightcoral\n\nWhen viewport > 600px:\n Media query condition false → ignored\n Only base style applies\n Result: lightblue\n\nCommon Media Features:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n@media (max-width: 600px) Viewport ≤ 600px\n@media (min-width: 768px) Viewport ≥ 768px\n@media (orientation: portrait) Height > Width\n@media (prefers-color-scheme: dark) OS dark mode\n@media (hover: hover) Device has hover",
"containerVsItem": ""
},
"validations": [
{
"type": "regex",
@@ -52,6 +57,11 @@
"codeSuffix": "}",
"solution": " font-size: 5vw;",
"previewContainer": "preview-area",
"concept": {
"explanation": "Viewport units (vw, vh, vmin, vmax) scale proportionally with the browser window—1vw equals 1% of viewport width, so 5vw on a 1000px screen equals 50px. As the user resizes, the browser recalculates in real-time: 5vw becomes 30px on a 600px screen or 70px on a 1400px screen, creating truly fluid typography without media query breakpoints. However, pure vw units can become too small on mobile or too large on wide screens. Production sites often use clamp(16px, 5vw, 48px) to set minimum and maximum bounds for readability.",
"diagram": "Viewport Width Units (vw)\n\nHow 5vw calculates across screen sizes:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nMobile (375px wide):\n1vw = 375px / 100 = 3.75px\n5vw = 3.75px × 5 = 18.75px\n┌──────────┐\n│ Text │ 18.75px font\n└──────────┘\n 375px\n\nTablet (768px wide):\n1vw = 768px / 100 = 7.68px\n5vw = 7.68px × 5 = 38.4px\n┌─────────────────────┐\n│ Text │ 38.4px font\n└─────────────────────┘\n 768px\n\nDesktop (1440px wide):\n1vw = 1440px / 100 = 14.4px\n5vw = 14.4px × 5 = 72px\n┌───────────────────────────────────────┐\n│ Text │ 72px font\n└───────────────────────────────────────┘\n 1440px\n\nViewport Unit Reference:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nvw = 1% of viewport width\nvh = 1% of viewport height\nvmin = 1% of viewport's smaller dimension\nvmax = 1% of viewport's larger dimension\n\nExample with 800px × 600px viewport:\n 1vw = 8px (1% of 800px)\n 1vh = 6px (1% of 600px)\n 1vmin = 6px (1% of smaller: 600px)\n 1vmax = 8px (1% of larger: 800px)\n\nProblem: Unbounded Scaling\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nPure vw can be too small or too large:\n\nMobile (320px): font-size: 5vw → 16px ✓ OK\nTablet (768px): font-size: 5vw → 38px ✓ OK\nDesktop (2560px): font-size: 5vw → 128px ✗ TOO BIG!\n\nSolution: Combine with clamp() or calc()\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nBetter approach:\nfont-size: clamp(16px, 5vw, 48px);\n ↓ ↓ ↓\n minimum fluid maximum\n\nResult across viewports:\n320px → 5vw = 16px → clamped to 16px (min)\n768px → 5vw = 38px → 38px (in range)\n2560px → 5vw = 128px → clamped to 48px (max)\n\nWhen to Use Fluid Typography:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nGood: Hero headings, banners, display text\nAvoid: Body text, UI elements (use rem instead)",
"containerVsItem": ""
},
"validations": [
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Set <kbd>font-size: 5vw</kbd>" }
]
@@ -59,16 +69,21 @@
{
"id": "responsive-3",
"title": "Responsive Grid",
"description": "Combine CSS Grid with <kbd>auto-fit</kbd> or <kbd>auto-fill</kbd> for responsive column layouts that automatically adjust the number of columns based on available space.",
"task": "Add <kbd>display: grid</kbd>, <kbd>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</kbd>, and <kbd>gap: 1rem</kbd> to <kbd>.features</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; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".features {\n ",
"codePrefix": "/* Create a responsive grid */\n.cards {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;",
"codeSuffix": "}",
"solution": " display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;",
"previewContainer": "preview-area",
"concept": {
"explanation": "The auto-fit keyword combined with minmax() creates intrinsically responsive grids that adapt without media queries. The pattern repeat(auto-fit, minmax(200px, 1fr)) tells the browser to create as many columns as will fit, where each is at least 200px but can grow to 1fr. The browser calculates how many 200px columns fit, distributes extra space equally, and automatically reflows columns as the viewport shrinks (4 → 3 → 2 → 1)—all without breakpoints. The key difference: auto-fit collapses empty columns to zero width, while auto-fill preserves empty column tracks.",
"diagram": "Auto-Fit with Minmax: Responsive Grid Without Media Queries\n\nHow repeat(auto-fit, minmax(200px, 1fr)) works:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nContainer: 900px wide (4 items)\n\nStep 1: Calculate how many 200px columns fit\n900px ÷ 200px = 4.5 → fits 4 columns\n\nStep 2: Distribute extra space with 1fr\n900px - (4 × 200px) = 100px extra\n100px ÷ 4 columns = 25px each\nFinal: 225px per column\n\n┌──────┬──────┬──────┬──────┐\n│ 1 │ 2 │ 3 │ 4 │\n└──────┴──────┴──────┴──────┘\n 225px 225px 225px 225px\n\nResponsive Behavior Across Viewports:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nWide (900px): 4 columns\n┌─────┬─────┬─────┬─────┐\n│ 1 │ 2 │ 3 │ 4 │\n└─────┴─────┴─────┴─────┘\n 225px each (200px + extra space)\n\nMedium (650px): 3 columns\n┌──────┬──────┬──────┐\n│ 1 │ 2 │ 3 │\n├──────┴──────┴──────┤\n│ 4 │ │ ← grows to fill\n└──────┴──────────────┘\n 216px each (200px + extra)\n\nNarrow (450px): 2 columns\n┌──────────┬──────────┐\n│ 1 │ 2 │\n├──────────┼──────────┤\n│ 3 │ 4 │\n└──────────┴──────────┘\n 225px each\n\nMobile (250px): 1 column\n┌────────────────────┐\n│ 1 │\n├────────────────────┤\n│ 2 │\n├────────────────────┤\n│ 3 │\n├────────────────────┤\n│ 4 │\n└────────────────────┘\n 250px (fills width)\n\nAuto-Fit vs Auto-Fill:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nWith 3 items in 900px container:\n\nauto-fit: Collapses empty tracks to zero\n┌────────────┬────────────┬────────────┐\n│ 1 │ 2 │ 3 │\n└────────────┴────────────┴────────────┘\n 300px 300px 300px\n ↑ Items expand to fill empty space\n\nauto-fill: Preserves empty tracks\n┌─────┬─────┬─────┬─────┐\n│ 1 │ 2 │ 3 │empty│ ← ghost column\n└─────┴─────┴─────┴─────┘\n 225px 225px 225px 0px (collapsed)\n\nBreakpoint Calculation:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nNatural breakpoints occur when columns reflow:\n4→3 columns: 600px (3 × 200px)\n3→2 columns: 400px (2 × 200px)\n2→1 column: 200px (1 × 200px)\n\nNo media queries needed—grid adapts automatically!",
"containerVsItem": "display: grid, grid-template-columns, and gap are CONTAINER properties. The auto-fit and minmax() functions define how the grid automatically creates responsive columns without requiring item-level overrides."
},
"validations": [
{
"type": "property_value",
@@ -101,6 +116,11 @@
"codeSuffix": "",
"solution": "@media (min-width: 768px) {\n .sidebar {\n width: 250px;\n }\n}",
"previewContainer": "preview-area",
"concept": {
"explanation": "Mobile-first design means writing base CSS for mobile devices first, then using min-width media queries to progressively enhance for larger screens. This approach has key advantages: mobile users download less CSS (desktop styles are in media queries they never trigger), it forces content prioritization for limited mobile screens, and the CSS cascade works in your favor—base styles apply everywhere while larger screens simply add enhancements. Using @media (min-width: 768px) means \"on screens 768px or wider, add these styles\"—the opposite of max-width which removes styles as screens shrink. This progressive enhancement pattern aligns with how users browse: most traffic is mobile, so optimize for that first, then layer on desktop features.",
"diagram": "Mobile-First vs Desktop-First Design\n\nMobile-First (Recommended):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nBase styles (mobile):\n.sidebar {\n width: 100%; /* Full width on mobile */\n padding: 1rem;\n}\n\nEnhancement for tablet+:\n@media (min-width: 768px) {\n .sidebar {\n width: 250px; /* Fixed sidebar on desktop */\n float: left;\n }\n}\n\nFlow: Mobile → Tablet → Desktop\n (add features as space increases)\n\n 0px 768px 1024px\n ├────────────────┼─────────────────┼──────►\n Base styles + Tablet styles + Desktop\n (mobile) styles\n\nDesktop-First (Not Recommended):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nBase styles (desktop):\n.sidebar {\n width: 250px; /* Desktop default */\n float: left;\n}\n\nOverrides for mobile:\n@media (max-width: 767px) {\n .sidebar {\n width: 100%; /* Undo desktop styles */\n float: none; /* More overrides needed */\n }\n}\n\nFlow: Desktop → Tablet → Mobile\n (remove features as space decreases)\n\n 0px 767px 1024px\n ├────────────────┼─────────────────┼──────►\n Mobile overrides │ Base styles\n (undo desktop) │ (desktop)\n\nPerformance Benefits:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nMobile-First CSS (320px phone):\n✓ Base styles: 2KB (downloaded)\n✗ @min-width 768: 1KB (ignored, not parsed)\n✗ @min-width 1024: 1KB (ignored)\n Total parsed: 2KB\n\nDesktop-First CSS (320px phone):\n✓ Base styles: 3KB (downloaded)\n✓ @max-width 767: 1KB (downloaded & parsed)\n Total parsed: 4KB (2x more!)\n\nMobile users save bandwidth and parsing time.\n\nCommon Mobile-First Breakpoints:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n/* Base: Mobile (0-767px) */\n/* Full-width, stacked layout */\n\n@media (min-width: 768px) {\n /* Tablet: Side-by-side for some elements */\n}\n\n@media (min-width: 1024px) {\n /* Desktop: Multi-column layouts */\n}\n\n@media (min-width: 1280px) {\n /* Large desktop: Max widths, more spacing */\n}\n\nContent Prioritization:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nMobile forces you to answer:\n\"What's ESSENTIAL for this page?\"\n\nMobile (320px): Desktop (1280px):\n┌──────────────┐ ┌─────┬──────────┬─────┐\n│ Header │ │ Ad │ Header │ User│\n├──────────────┤ ├─────┴──────────┴─────┤\n│ Content │ │ Side │ Content │ Side│\n│ (core) │ │ bar │ (core) │ bar │\n├──────────────┤ │ ├──────────┤ │\n│ Footer │ │ │ Related │ │\n└──────────────┘ └──────┴──────────┴─────┘\n ↑ Extra features added\n Core only when space allows\n\nWhy Min-Width Is Better:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n✓ Less CSS for mobile (faster page loads)\n✓ Progressive enhancement (build up, not tear down)\n✓ Aligns with CSS cascade (adds, not overrides)\n✓ Encourages content-first thinking\n✓ Easier to maintain (fewer conflicting rules)",
"containerVsItem": ""
},
"validations": [
{
"type": "regex",

View File

@@ -1,92 +0,0 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "css-gradients",
"title": "CSS Gradients",
"description": "Create smooth color transitions with CSS gradients.",
"difficulty": "intermediate",
"lessons": [
{
"id": "gradients-1",
"title": "Linear Gradient",
"description": "Gradients create smooth transitions between colors. The <kbd>linear-gradient()</kbd> function creates a gradient along a straight line.<br><br><strong>Basic syntax:</strong><br><pre>background: linear-gradient(color1, color2);</pre><br>By default, gradients flow from top to bottom.",
"task": "Add a gradient background from <kbd>coral</kbd> to <kbd>gold</kbd>.",
"previewHTML": "<div class=\"card\"><h3>Summer Sale</h3><p>Up to 50% off</p></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .card { padding: 2rem; border-radius: 12px; color: white; text-shadow: 0 1px 2px rgba(0,0,0,0.2); } .card h3 { margin: 0 0 8px; font-size: 1.5rem; } .card p { margin: 0; opacity: 0.9; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "background: linear-gradient(coral, gold);",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "linear-gradient",
"message": "Use <kbd>linear-gradient()</kbd>"
},
{
"type": "contains",
"value": "coral",
"message": "Include <kbd>coral</kbd> as the first color"
},
{
"type": "contains",
"value": "gold",
"message": "Include <kbd>gold</kbd> as the second color"
}
]
},
{
"id": "gradients-2",
"title": "Gradient Direction",
"description": "Control the gradient direction by adding an angle or keyword before the colors.<br><br><strong>Keywords:</strong> <kbd>to right</kbd>, <kbd>to left</kbd>, <kbd>to bottom right</kbd><br><strong>Angles:</strong> <kbd>45deg</kbd>, <kbd>90deg</kbd>, <kbd>180deg</kbd><br><br><pre>background: linear-gradient(to right, blue, purple);</pre>",
"task": "Make the gradient flow from left to right using <kbd>to right</kbd>.",
"previewHTML": "<button class=\"btn\">Get Started</button>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 2rem; } .btn { padding: 1rem 2rem; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; color: white; cursor: pointer; }",
"sandboxCSS": "",
"codePrefix": ".btn {\n background: linear-gradient(",
"initialCode": "",
"codeSuffix": ", steelblue, mediumseagreen);\n}",
"solution": "to right",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "to right",
"message": "Add <kbd>to right</kbd> to set the direction"
}
]
},
{
"id": "gradients-3",
"title": "Radial Gradient",
"description": "The <kbd>radial-gradient()</kbd> function creates a gradient that radiates from a center point outward in a circular or elliptical pattern.<br><br><pre>background: radial-gradient(circle, white, steelblue);</pre><br>Add <kbd>circle</kbd> for a perfect circular gradient.",
"task": "Create a radial gradient from <kbd>white</kbd> to <kbd>steelblue</kbd>.",
"previewHTML": "<div class=\"orb\"></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 2rem; display: flex; justify-content: center; } .orb { width: 150px; height: 150px; border-radius: 50%; box-shadow: 0 8px 32px rgba(70, 130, 180, 0.4); }",
"sandboxCSS": "",
"codePrefix": ".orb {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "background: radial-gradient(circle, white, steelblue);",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "radial-gradient",
"message": "Use <kbd>radial-gradient()</kbd>"
},
{
"type": "contains",
"value": "white",
"message": "Start with <kbd>white</kbd>"
},
{
"type": "contains",
"value": "steelblue",
"message": "End with <kbd>steelblue</kbd>"
}
]
}
]
}

View File

@@ -16,6 +16,10 @@
"sandboxCSS": "",
"initialCode": "",
"previewContainer": "preview-area",
"concept": {
"explanation": "Tailwind replaces custom CSS classes with pre-built utility classes that apply single CSS properties. Instead of writing .my-box { background-color: #3b82f6; } in a CSS file, you apply bg-blue-500 directly in HTML. The class name bg-blue-500 maps to a specific hex color (#3b82f6) from Tailwind's design system, ensuring consistent colors across your project. This eliminates the need to name things and context-switch between HTML and CSS files.",
"diagram": "Traditional CSS vs Tailwind\n\nTraditional CSS Approach:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nHTML: <div class=\"hero-box\">\nCSS: .hero-box { background-color: #3b82f6; }\n ↑ Custom name ↑ Custom color\n\nTailwind Approach:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nHTML: <div class=\"bg-blue-500\">\n ↑ Pre-built utility (no CSS file needed)\n\nColor Scale (50-950):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nbg-blue-50 ░░░ Lightest\nbg-blue-500 ███ Medium (default)\nbg-blue-950 ███ Darkest\n\nBenefit: No naming, no context-switching"
},
"validations": [
{
"type": "contains_class",
@@ -34,6 +38,10 @@
"sandboxCSS": "/* Traditional CSS approach:\n.card {\n background-color: white;\n padding: 1rem;\n border-radius: 0.25rem;\n}\n*/",
"initialCode": "",
"previewContainer": "preview-area",
"concept": {
"explanation": "Utility-first CSS inverts traditional workflow: instead of semantic class names (.card, .button) that group multiple properties in separate CSS files, you compose components by combining single-purpose utilities directly in markup. This eliminates three major CSS problems: (1) naming things (no more .primary-btn vs .btn-primary debates), (2) specificity wars (utilities have equal specificity), and (3) unused CSS (only utilities you use get included). The tradeoff is longer HTML class lists, but Tailwind argues this is outweighed by no context-switching and automatic consistency.",
"diagram": "Traditional vs Utility-First Workflow\n\nTraditional Semantic CSS:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n1. Write HTML: <div class=\"card\">\n2. Switch to CSS: .card { ... }\n3. Name component: .card, .primary-card?\n4. Fight cascade: .card.special vs .special.card\n5. Dead CSS grows: Old .card variants pile up\n\nUtility-First Approach:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n1. Compose in HTML: <div class=\"bg-white p-4 rounded shadow-sm\">\n2. No CSS file needed (utilities pre-defined)\n3. No naming required (describe what it looks like)\n4. No specificity issues (all utilities = 0,0,1,0)\n5. PurgeCSS removes unused utilities automatically\n\nProblems Solved:\n✓ Naming: bg-white (descriptive, not semantic)\n✓ Specificity: All utilities equal weight\n✓ Dead CSS: Tree-shaking removes unused\n✓ Consistency: Design system baked in\n\nTradeoff:\n✗ Longer class lists in HTML\n✓ But: No CSS file, no context-switching"
},
"validations": [
{
"type": "contains_class",
@@ -67,6 +75,10 @@
"sandboxCSS": "/* Traditional CSS would be:\nh1 {\n color: #2563eb;\n font-size: 1.5rem;\n font-weight: 700;\n}\n*/",
"initialCode": "",
"previewContainer": "preview-area",
"concept": {
"explanation": "Tailwind's text utilities follow a predictable naming convention that maps directly to CSS properties, making them easy to learn and remember. The pattern property-value (like text-blue-600 for color, text-2xl for size) creates a consistent mental model across all utilities. The color shade system (50-950) ensures accessible contrast ratios: lighter shades (50-300) for backgrounds, medium shades (400-600) for UI elements, darker shades (700-950) for text. This built-in design system prevents arbitrary color choices and maintains visual consistency across your entire application.",
"diagram": "Text Utility Naming Patterns\n\nColor Pattern: text-{color}-{shade}\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\ntext-blue-600 → color: #2563eb\n ↑ ↑ ↑\n prop color shade\n\nShade Scale (contrast-optimized):\n50-300 ░░░ Light (backgrounds)\n400-600 ███ Medium (UI elements)\n700-950 ███ Dark (text, WCAG compliant)\n\nSize Pattern: text-{size}\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\ntext-sm → font-size: 0.875rem (14px)\ntext-base → font-size: 1rem (16px)\ntext-2xl → font-size: 1.5rem (24px)\ntext-9xl → font-size: 8rem (128px)\n\nWeight Pattern: font-{weight}\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nfont-normal → font-weight: 400\nfont-semibold → font-weight: 600\nfont-bold → font-weight: 700\n\nBenefit: Predictable, consistent system"
},
"validations": [
{
"type": "contains_class",
@@ -95,6 +107,10 @@
"sandboxCSS": "/* Traditional CSS equivalent:\nbutton {\n padding-left: 1.5rem;\n padding-right: 1.5rem;\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n margin-left: auto;\n margin-right: auto;\n}\n*/",
"initialCode": "",
"previewContainer": "preview-area",
"concept": {
"explanation": "Tailwind's spacing scale uses base-4 (0.25rem = 4px increments) because it creates harmonious visual rhythm and aligns with the 8pt grid system used by most design tools. The directional shorthands (px for horizontal, py for vertical) map to common CSS patterns but use intuitive abbreviations: p for padding, m for margin, x for horizontal axis, y for vertical axis. This system is more concise than CSS (px-6 vs padding-left: 1.5rem; padding-right: 1.5rem;) while being completely predictable: p-4 is always 1rem (4 × 0.25rem), regardless of context.",
"diagram": "Spacing Scale & Directional Shorthands\n\nBase-4 Scale (n × 0.25rem):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\np-1 0.25rem 4px ▏\np-2 0.5rem 8px ▎\np-4 1rem 16px ▌ ← Common default\np-6 1.5rem 24px ▊\np-8 2rem 32px █\np-12 3rem 48px ██\np-16 4rem 64px ████\n\nDirectional Shorthands:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n ┌─── pt-4 ───┐\n │ │\npl-4 ────►┤ p-4 / px-6├◄──── pr-4\n │ py-3 │\n │ │\n └─── pb-4 ───┘\n\np-4 All sides (shorthand)\npx-6 Horizontal (x-axis: left + right)\npy-3 Vertical (y-axis: top + bottom)\npt/pr/pb/pl Individual sides\n\nmx-auto Horizontal centering\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n←auto→ [element] ←auto→\n\nBenefit: Consistent rhythm, predictable values"
},
"validations": [
{
"type": "contains_class",
@@ -123,6 +139,10 @@
"sandboxCSS": "/* Traditional CSS would require media queries:\n.responsive-box {\n width: 100%;\n font-size: 1.125rem;\n}\n@media (min-width: 768px) {\n .responsive-box {\n width: 50%;\n font-size: 1.25rem;\n }\n}\n@media (min-width: 1024px) {\n .responsive-box {\n width: 33.333333%;\n font-size: 1.5rem;\n }\n}\n*/",
"initialCode": "",
"previewContainer": "preview-area",
"concept": {
"explanation": "Tailwind's responsive utilities eliminate the need to write media queries by using breakpoint prefixes that compile to min-width media queries. The mobile-first approach means base utilities apply to all screen sizes, then prefixed utilities (md:, lg:) override them at larger breakpoints. This inverts traditional CSS where you write desktop styles first, then override for mobile. Writing w-full md:w-1/2 lg:w-1/3 in HTML is more maintainable than context-switching to a CSS file and writing three separate media query blocks, especially when each breakpoint change is visible right where the element is defined.",
"diagram": "Responsive Breakpoint System\n\nMobile-First Cascade:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nw-full md:w-1/2 lg:w-1/3\n ↓ ↓ ↓\nBase @768px+ @1024px+\n\nBreakpoint Scale:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nsm: 640px Mobile landscape\nmd: 768px Tablet portrait\nlg: 1024px Tablet landscape / Desktop\nxl: 1280px Large desktop\n2xl: 1536px Extra large desktop\n\nCompiled CSS (Tailwind generates):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n.w-full { width: 100%; }\n\n@media (min-width: 768px) {\n .md\\:w-1\\/2 { width: 50%; }\n}\n\n@media (min-width: 1024px) {\n .lg\\:w-1\\/3 { width: 33.333%; }\n}\n\nVisual Effect:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nMobile (<768px): [████████████] 100%\nTablet (768-1024): [██████] 50%\nDesktop (1024px+): [████] 33%\n\nBenefit: No media queries, inline responsive logic"
},
"validations": [
{
"type": "contains_class",

View File

@@ -1,108 +0,0 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "css-filters",
"title": "CSS Filters",
"description": "Apply visual effects like blur, brightness, and shadows with CSS filters.",
"difficulty": "intermediate",
"lessons": [
{
"id": "filters-1",
"title": "Blur Filter",
"description": "The <kbd>filter</kbd> property applies visual effects to elements. The <kbd>blur()</kbd> function creates a Gaussian blur effect.<br><br><pre>filter: blur(4px);</pre><br>Higher values create more blur. This is great for backgrounds or creating depth.",
"task": "Blur the background image using <kbd>filter: blur(4px)</kbd>.",
"previewHTML": "<div class=\"bg\"></div><div class=\"content\"><h2>Welcome</h2></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; height: 200px; position: relative; overflow: hidden; } .bg { position: absolute; inset: 0; background: linear-gradient(45deg, coral, gold, steelblue); } .content { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; } .content h2 { color: white; text-shadow: 0 2px 8px rgba(0,0,0,0.3); margin: 0; }",
"sandboxCSS": "",
"codePrefix": ".bg {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "filter: blur(4px);",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "filter", "expected": "blur(4px)" },
"message": "Set <kbd>filter: blur(4px)</kbd>"
}
]
},
{
"id": "filters-2",
"title": "Grayscale Filter",
"description": "The <kbd>grayscale()</kbd> function removes color from an element. Use values from <kbd>0%</kbd> (full color) to <kbd>100%</kbd> (fully grayscale).<br><br><pre>filter: grayscale(100%);</pre><br>Great for hover effects or disabled states.",
"task": "Make the image grayscale with <kbd>filter: grayscale(100%)</kbd>.",
"previewHTML": "<div class=\"photo\"></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .photo { width: 200px; height: 150px; background: linear-gradient(135deg, coral 0%, gold 50%, steelblue 100%); border-radius: 8px; }",
"sandboxCSS": "",
"codePrefix": ".photo {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "filter: grayscale(100%);",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "grayscale",
"message": "Use <kbd>grayscale()</kbd> filter"
},
{
"type": "contains",
"value": "100%",
"message": "Set to <kbd>100%</kbd> for full grayscale"
}
]
},
{
"id": "filters-3",
"title": "Brightness Filter",
"description": "The <kbd>brightness()</kbd> function adjusts how bright an element appears. Values below <kbd>100%</kbd> darken, above <kbd>100%</kbd> brighten.<br><br><pre>filter: brightness(150%);</pre>",
"task": "Brighten the card with <kbd>filter: brightness(120%)</kbd>.",
"previewHTML": "<div class=\"card\"><span>Featured</span></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #1a1a2e; } .card { padding: 2rem; background: linear-gradient(135deg, #4a4a6a, #2a2a4a); border-radius: 12px; text-align: center; } .card span { color: gold; font-weight: 600; text-transform: uppercase; letter-spacing: 2px; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "filter: brightness(120%);",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "brightness",
"message": "Use <kbd>brightness()</kbd> filter"
},
{
"type": "contains",
"value": "120%",
"message": "Set to <kbd>120%</kbd>"
}
]
},
{
"id": "filters-4",
"title": "Drop Shadow",
"description": "The <kbd>drop-shadow()</kbd> filter creates a shadow that follows the shape of the element, including transparency. Unlike <kbd>box-shadow</kbd>, it works on images with transparent backgrounds.<br><br><pre>filter: drop-shadow(2px 4px 6px black);</pre>",
"task": "Add a drop shadow with <kbd>filter: drop-shadow(4px 4px 8px gray)</kbd>.",
"previewHTML": "<div class=\"icon\">★</div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 2rem; display: flex; justify-content: center; } .icon { font-size: 4rem; color: gold; }",
"sandboxCSS": "",
"codePrefix": ".icon {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "filter: drop-shadow(4px 4px 8px gray);",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "drop-shadow",
"message": "Use <kbd>drop-shadow()</kbd> filter"
},
{
"type": "contains",
"value": "4px 4px 8px",
"message": "Set shadow offset and blur"
}
]
}
]
}

View File

@@ -1,98 +0,0 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "css-positioning",
"title": "CSS Positioning",
"description": "Control element placement with CSS positioning properties.",
"difficulty": "intermediate",
"lessons": [
{
"id": "position-1",
"title": "Relative Position",
"description": "The <kbd>position</kbd> property controls how elements are placed. <kbd>relative</kbd> keeps the element in normal flow but allows you to offset it with <kbd>top</kbd>, <kbd>right</kbd>, <kbd>bottom</kbd>, <kbd>left</kbd>.<br><br><pre>.box {\n position: relative;\n top: 10px;\n}</pre>",
"task": "Make the badge position relative so we can offset it.",
"previewHTML": "<div class=\"card\"><span class=\"badge\">NEW</span><h3>Product</h3></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .card { padding: 1rem; background: white; border: 2px solid #eee; border-radius: 8px; } .card h3 { margin: 0; } .badge { display: inline-block; padding: 2px 8px; background: coral; color: white; font-size: 0.7rem; font-weight: bold; border-radius: 4px; }",
"sandboxCSS": "",
"codePrefix": ".badge {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "position: relative;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "position", "expected": "relative" },
"message": "Set <kbd>position: relative</kbd>"
}
]
},
{
"id": "position-2",
"title": "Offset Properties",
"description": "With <kbd>position: relative</kbd>, use offset properties to nudge the element from its original position:<br><br><kbd>top</kbd> - pushes down from top<br><kbd>left</kbd> - pushes right from left<br><br>Negative values move in the opposite direction.",
"task": "Move the badge up with <kbd>top: -8px</kbd>.",
"previewHTML": "<div class=\"card\"><span class=\"badge\">NEW</span><h3>Product</h3></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .card { padding: 1rem; background: white; border: 2px solid #eee; border-radius: 8px; } .card h3 { margin: 0; } .badge { display: inline-block; padding: 2px 8px; background: coral; color: white; font-size: 0.7rem; font-weight: bold; border-radius: 4px; position: relative; }",
"sandboxCSS": "",
"codePrefix": ".badge {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "top: -8px;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "top", "expected": "-8px" },
"message": "Set <kbd>top: -8px</kbd>"
}
]
},
{
"id": "position-3",
"title": "Absolute Position",
"description": "<kbd>position: absolute</kbd> removes the element from normal flow and positions it relative to its nearest positioned ancestor (or the viewport if none exists).<br><br>Always set a parent to <kbd>position: relative</kbd> to contain absolute children.",
"task": "Position the close button absolutely.",
"previewHTML": "<div class=\"modal\"><button class=\"close\">×</button><h3>Modal</h3><p>Content here</p></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .modal { position: relative; padding: 2rem; background: white; border-radius: 12px; box-shadow: 0 4px 24px rgba(0,0,0,0.15); max-width: 250px; } .modal h3 { margin: 0 0 8px; } .modal p { margin: 0; color: #666; } .close { width: 32px; height: 32px; border: none; background: #f5f5f5; border-radius: 50%; font-size: 1.2rem; cursor: pointer; }",
"sandboxCSS": "",
"codePrefix": ".close {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "position: absolute;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "position", "expected": "absolute" },
"message": "Set <kbd>position: absolute</kbd>"
}
]
},
{
"id": "position-4",
"title": "Placing Absolute Elements",
"description": "Combine <kbd>position: absolute</kbd> with offset properties to place elements precisely.<br><br><pre>.close {\n position: absolute;\n top: 8px;\n right: 8px;\n}</pre>",
"task": "Move the close button to the top right corner with <kbd>top: 8px</kbd> and <kbd>right: 8px</kbd>.",
"previewHTML": "<div class=\"modal\"><button class=\"close\">×</button><h3>Modal</h3><p>Content here</p></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .modal { position: relative; padding: 2rem; background: white; border-radius: 12px; box-shadow: 0 4px 24px rgba(0,0,0,0.15); max-width: 250px; } .modal h3 { margin: 0 0 8px; } .modal p { margin: 0; color: #666; } .close { position: absolute; width: 32px; height: 32px; border: none; background: #f5f5f5; border-radius: 50%; font-size: 1.2rem; cursor: pointer; }",
"sandboxCSS": "",
"codePrefix": ".close {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "top: 8px;\n right: 8px;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "top", "expected": "8px" },
"message": "Set <kbd>top: 8px</kbd>"
},
{
"type": "property_value",
"value": { "property": "right", "expected": "8px" },
"message": "Set <kbd>right: 8px</kbd>"
}
]
}
]
}

View File

@@ -1,113 +0,0 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "css-pseudo-elements",
"title": "CSS Pseudo-elements",
"description": "Create decorative elements and style specific parts of content with pseudo-elements.",
"difficulty": "intermediate",
"lessons": [
{
"id": "pseudo-1",
"title": "The ::before Element",
"description": "Pseudo-elements let you style specific parts of an element. <kbd>::before</kbd> creates a virtual element as the first child.<br><br>It requires the <kbd>content</kbd> property to display anything (even if empty).<br><br><pre>.item::before {\n content: \"→ \";\n}</pre>",
"task": "Add a bullet before each list item using <kbd>::before</kbd> with <kbd>content: \"• \"</kbd>.",
"previewHTML": "<ul class=\"list\"><li>First item</li><li>Second item</li><li>Third item</li></ul>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .list { list-style: none; padding: 0; margin: 0; } .list li { padding: 8px 0; }",
"sandboxCSS": "",
"codePrefix": ".list li::before {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "content: \"• \";",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "content",
"message": "Use the <kbd>content</kbd> property"
},
{
"type": "contains",
"value": "•",
"message": "Add a bullet character <kbd>•</kbd>"
}
]
},
{
"id": "pseudo-2",
"title": "Styling ::before",
"description": "Pseudo-elements can be styled like any element. Add color, size, margins, and more.<br><br><pre>.item::before {\n content: \"★\";\n color: gold;\n margin-right: 8px;\n}</pre>",
"task": "Style the bullet with <kbd>color: coral</kbd>.",
"previewHTML": "<ul class=\"list\"><li>First item</li><li>Second item</li><li>Third item</li></ul>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .list { list-style: none; padding: 0; margin: 0; } .list li { padding: 8px 0; } .list li::before { content: \"• \"; }",
"sandboxCSS": "",
"codePrefix": ".list li::before {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "color: coral;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "color", "expected": "coral" },
"message": "Set <kbd>color: coral</kbd>"
}
]
},
{
"id": "pseudo-3",
"title": "The ::after Element",
"description": "<kbd>::after</kbd> works like <kbd>::before</kbd> but inserts content as the last child. Common uses include badges, icons, or decorative elements.<br><br><pre>.new::after {\n content: \" ✓\";\n color: green;\n}</pre>",
"task": "Add a checkmark after completed items with <kbd>content: \" ✓\"</kbd>.",
"previewHTML": "<ul class=\"list\"><li class=\"done\">Buy groceries</li><li class=\"done\">Walk the dog</li><li>Read a book</li></ul>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .list { list-style: none; padding: 0; margin: 0; } .list li { padding: 8px 0; }",
"sandboxCSS": "",
"codePrefix": ".done::after {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "content: \" ✓\";",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "content",
"message": "Use the <kbd>content</kbd> property"
},
{
"type": "contains",
"value": "✓",
"message": "Add a checkmark <kbd>✓</kbd>"
}
]
},
{
"id": "pseudo-4",
"title": "Decorative Lines",
"description": "Pseudo-elements with <kbd>content: \"\"</kbd> can create decorative shapes when combined with width, height, and background.<br><br><pre>.title::after {\n content: \"\";\n display: block;\n width: 50px;\n height: 3px;\n background: coral;\n}</pre>",
"task": "Create an underline decoration with <kbd>width: 40px</kbd>, <kbd>height: 3px</kbd>, and <kbd>background: steelblue</kbd>.",
"previewHTML": "<h2 class=\"title\">About Us</h2><p>We build great things.</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .title { margin: 0 0 1rem; } .title::after { content: \"\"; display: block; margin-top: 8px; } p { margin: 0; color: #666; }",
"sandboxCSS": "",
"codePrefix": ".title::after {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "width: 40px;\n height: 3px;\n background: steelblue;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "width", "expected": "40px" },
"message": "Set <kbd>width: 40px</kbd>"
},
{
"type": "property_value",
"value": { "property": "height", "expected": "3px" },
"message": "Set <kbd>height: 3px</kbd>"
},
{
"type": "property_value",
"value": { "property": "background", "expected": "steelblue" },
"message": "Set <kbd>background: steelblue</kbd>"
}
]
}
]
}

View File

@@ -17,6 +17,10 @@
"initialCode": "<p>This is a paragraph with an important word.</p>",
"solution": "<p>This is a paragraph with an <strong>important</strong> word.</p>",
"previewContainer": "preview-area",
"concept": {
"explanation": "The browser's layout engine treats block and inline elements fundamentally differently. Block elements create a rectangular box that starts on a new line and expands to fill available width, stacking vertically like building blocks. Inline elements flow horizontally within text content, wrapping to new lines only when they run out of space—like words in a paragraph. This distinction controls document flow: use block for structure (sections, paragraphs) and inline for content emphasis (bold, links) without breaking the text flow.",
"diagram": "Block vs Inline Layout\n\nBlock elements (vertical stacking):\n┌─────────────────────────────┐\n│ <div> Full width block │ ← New line\n└─────────────────────────────┘\n┌─────────────────────────────┐\n│ <p> Another block element │ ← New line\n└─────────────────────────────┘\n\nInline elements (horizontal flow):\n┌─────────────────────────────┐\n│ Text with <a>link</a> and │\n│ <strong>bold</strong> flows │ ← Wraps naturally\n│ like words in a sentence. │\n└─────────────────────────────┘\n\nKey differences:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nBlock: New line, full width\nInline: Same line, auto width\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
},
"validations": [
{
"type": "element_exists",
@@ -41,6 +45,10 @@
"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>",
"previewContainer": "preview-area",
"concept": {
"explanation": "Semantic HTML elements convey meaning about their content, not just appearance. Screen readers use semantic tags to help blind users navigate (\"skip to main content\" relies on <main>), search engines rank pages higher when structure is clear (<article> signals important content), and developers understand code faster when tags describe purpose. Using <header> instead of <div class=\"header\"> gives the same visual result but adds machine-readable meaning that assistive technology and search crawlers can understand. This is the foundation of accessible, SEO-friendly web development.",
"diagram": "Semantic Page Structure\n\n┌─────────────────────────────┐\n│ <header> │ ← Page header\n│ <h1>Site Title</h1> │ (branding, logo)\n│ <nav>Menu</nav> │ (navigation)\n└─────────────────────────────┘\n┌─────────────────────────────┐\n│ <main> │ ← Primary content\n│ <article>Blog Post</article> (unique per page)\n│ <section>Comments</section> (landmarks)\n└─────────────────────────────┘\n┌─────────────────────────────┐\n│ <footer> │ ← Page footer\n│ Copyright, links │ (metadata)\n└─────────────────────────────┘\n\nBenefits:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nAccessibility: Screen readers\nSEO: Search ranking\nMaintainability: Self-documenting\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
},
"validations": [
{
"type": "element_exists",
@@ -63,6 +71,39 @@
"message": "Add an <kbd>&lt;h1&gt;</kbd> heading inside your header"
}
]
},
{
"id": "div-vs-span",
"title": "div & span",
"description": "When you need a container without semantic meaning:<br><br><kbd>&lt;div&gt;</kbd> - Generic block container (for layout/grouping)<br><kbd>&lt;span&gt;</kbd> - Generic inline container (for styling text portions)<br><br>Use semantic elements when possible, div/span when no semantic element fits.",
"task": "Wrap the word 'highlighted' in a <kbd>&lt;span&gt;</kbd> to style it differently. Wrap the whole quote in a <kbd>&lt;div&gt;</kbd>.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: Georgia, serif; padding: 20px; } div { background: #f5f5f5; padding: 15px; border-left: 4px solid #1976d2; } span { background: #fff59d; padding: 2px 4px; }",
"sandboxCSS": "",
"initialCode": "The most highlighted moment was unforgettable.",
"solution": "<div>The most <span>highlighted</span> moment was unforgettable.</div>",
"previewContainer": "preview-area",
"concept": {
"explanation": "While semantic elements describe content meaning, <div> and <span> are semantically neutral containers used purely for styling or JavaScript hooks when no semantic element fits. Use <div> to group block-level content for layout purposes (like creating a grid wrapper) and <span> to target inline text portions for styling (like highlighting a word). However, always ask first: is there a better semantic choice? For example, use <article> instead of <div class=\"post\">, or <strong> instead of <span class=\"bold\">. Generic containers should be your last resort, not your first choice.",
"diagram": "When to Use Generic Containers\n\nSemantic First (Preferred):\n✓ <header> instead of <div class=\"header\">\n✓ <nav> instead of <div class=\"nav\">\n✓ <strong> instead of <span class=\"bold\">\n✓ <em> instead of <span class=\"italic\">\n\nGeneric When Needed:\n✓ <div> Layout wrapper (grid/flex)\n✓ <span> Style hook (color/bg only)\n\nDecision Tree:\n┌─────────────────────────────┐\n│ Does a semantic tag exist? │\n│ ↓ Yes ↓ No │\n│ Use it Use div/span │\n└─────────────────────────────┘\n\nPrinciple: Meaning > Presentation"
},
"validations": [
{
"type": "element_exists",
"value": "div",
"message": "Wrap everything in a <kbd>&lt;div&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "span",
"message": "Add a <kbd>&lt;span&gt;</kbd> around the word <kbd>highlighted</kbd>"
},
{
"type": "element_text",
"value": { "selector": "span", "text": "highlighted" },
"message": "The <kbd>&lt;span&gt;</kbd> should contain the word <kbd>highlighted</kbd>"
}
]
}
]
}

View File

@@ -17,6 +17,10 @@
"initialCode": "",
"solution": "<form>\n <label for=\"name\">Name:</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n</form>",
"previewContainer": "preview-area",
"concept": {
"explanation": "The label-for-id connection creates an accessible relationship that assistive technologies understand. When a screen reader encounters <label for=\"name\">, it announces \"Name, edit text\" so blind users know what to enter. Clicking the label also focuses the input, giving users a larger click target (helpful on mobile and for motor disabilities). The name attribute identifies the field when submitting data to a server, while the id attribute creates the accessibility link—both serve different but essential purposes.",
"diagram": "Form Accessibility Chain\n\n┌─────────────────────────────┐\n│ <label for=\"email\"> │ ← Click target\n│ Email: │ (focuses input)\n│ </label> │\n└────────────┬────────────────┘\n │ for=\"email\"\n ↓ connects to id\n┌────────────┴────────────────┐\n│ <input id=\"email\" │ ← Accessibility link\n│ name=\"email\"> │ (server identifier)\n└─────────────────────────────┘\n\nWhat Happens:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n1. User clicks label\n2. Browser finds matching id\n3. Input receives focus\n4. Screen reader announces label\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nWhy Both Attributes?\nid → Accessibility (label link)\nname → Server data (form submit)"
},
"validations": [
{
"type": "element_exists",
@@ -56,6 +60,10 @@
"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>",
"previewContainer": "preview-area",
"concept": {
"explanation": "Input types give the browser semantic understanding of what data to expect, enabling native features without JavaScript. On mobile, type=\"email\" shows a keyboard with @ and .com shortcuts, type=\"tel\" displays a numeric dialpad, and type=\"number\" shows +/- controls. The browser also provides free validation: type=\"email\" automatically checks for @ symbols and rejects invalid formats on submit. Using semantic input types is the foundation of progressive enhancement—you get better UX, accessibility, and validation for free.",
"diagram": "Input Type Benefits\n\nMobile Keyboard Optimization:\n┌────────────────────────────┐\ntype=\"text\" → QWERTY │ ABC...\ntype=\"email\" → @ .com keys │ user@domain\ntype=\"tel\" → Dialpad │ 0-9 only\ntype=\"number\" → +/- arrows │ Steppers\ntype=\"url\" → .com / www │ https://\n└────────────────────────────┘\n\nNative Validation (Free!):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nemail → Must contain @\nurl → Must start http://\nnumber → Numeric characters only\ntel → No validation (varies)\npassword → Hides characters ••••\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nAccessibility:\nScreen readers announce input type\n\"Email, edit text\" vs just \"edit text\""
},
"validations": [
{
"type": "element_exists",
@@ -85,6 +93,10 @@
"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>",
"previewContainer": "preview-area",
"concept": {
"explanation": "When clicked, a submit button triggers the form's native submit event, which validates all required fields, checks input constraints (minlength, pattern, etc.), and prevents submission if validation fails—all without JavaScript. The browser handles Enter key submission automatically when focus is in any text input. Prefer <button type=\"submit\"> over <input type=\"submit\"> because buttons can contain HTML (icons, spinners during loading), while input elements can only display plain text set via the value attribute.",
"diagram": "Form Submission Flow\n\nUser Action:\n┌────────────────────────────┐\n│ <button type=\"submit\"> │ ← Click or\n│ Sign In │ Enter key\n│ </button> │ pressed\n└────────────┬───────────────┘\n │\n ↓ Triggers\n┌────────────┴───────────────┐\n│ Browser Validation: │\n│ ✓ Check required fields │\n│ ✓ Validate input types │\n│ ✓ Test constraints │\n│ ✓ Match patterns │\n└────────────┬───────────────┘\n │\n ┌──────┴──────┐\n ✓ Valid ✗ Invalid\n │ │\n ↓ ↓\n Submit form Show error\n (POST data) (block submit)\n\nButton vs Input:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n<button> → Can contain HTML\n<input> → Plain text only\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
},
"validations": [
{
"type": "element_exists",

View File

@@ -1,32 +1,122 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "html-forms-validation",
"title": "Form Validation",
"description": "Use HTML5 built-in validation for better user experience",
"title": "HTML Validation",
"description": "Learn HTML5 built-in form validation attributes",
"mode": "html",
"difficulty": "beginner",
"difficulty": "intermediate",
"lessons": [
{
"id": "required-fields",
"title": "Required Fields",
"description": "The <kbd>required</kbd> attribute prevents form submission if the field is empty. The browser shows a validation message automatically - no JavaScript needed!<br><br>Add it to any input that must be filled:<br><kbd>&lt;input type=\"text\" required&gt;</kbd>",
"task": "Make both the name and email fields required by adding the <kbd>required</kbd> attribute to each input.",
"description": "The <kbd>required</kbd> attribute prevents form submission if the field is empty.<br><br>Add it to any input that must be filled:<br><kbd>&lt;input type=\"text\" required&gt;</kbd><br><br>The browser shows a validation message automatically.",
"task": "Make both the name and email fields required by adding the <kbd>required</kbd> attribute.",
"previewHTML": "",
"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; }",
"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; }",
"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\">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>",
"previewContainer": "preview-area",
"concept": {
"explanation": "The required attribute activates the browser's Constraint Validation API, which checks field values before allowing form submission. When a user tries to submit with empty required fields, the browser automatically focuses the first invalid field, displays a localized error message (\"Please fill out this field\" in English), and blocks the submit event—no JavaScript needed. The :invalid CSS pseudo-class lets you style invalid fields (like red borders), and screen readers announce required fields as \"Email, required, edit text\" so all users know which fields are mandatory.",
"diagram": "Native Validation Flow\n\nBefore Submit:\n┌────────────────────────────┐\n│ <input required> │ ← Browser monitors\n│ [empty] │ validity state\n└────────────────────────────┘\n :invalid pseudo-class\n\nOn Submit Click:\n┌────────────────────────────┐\n│ Browser checks: │\n│ ✓ Is field filled? │\n│ ✗ Empty → INVALID │\n└────────────┬───────────────┘\n │\n ↓ Blocks submit\n┌────────────┴───────────────┐\n│ [!] Please fill out this │ ← Localized\n│ field │ browser message\n└────────────────────────────┘\n Focus moved here\n\nCSS States Available:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n:valid → Green border\n:invalid → Red border\n:required → Asterisk icon\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
},
"validations": [
{
"type": "attribute_value",
"value": { "selector": "input[name='name']", "attr": "required", "value": true },
"message": "Add <kbd>required</kbd> to the name input"
"message": "Add the <kbd>required</kbd> attribute to the name input"
},
{
"type": "attribute_value",
"value": { "selector": "input[name='email']", "attr": "required", "value": true },
"message": "Add <kbd>required</kbd> to the email input"
"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",
"concept": {
"explanation": "Constraint attributes define validation rules that the browser enforces automatically. The minlength attribute triggers :invalid state and blocks submission if the value is shorter than 8 characters, while maxlength physically prevents typing beyond 20 characters (a hard limit, not just validation). The pattern attribute accepts regex for complex rules like \"uppercase + lowercase + number\" without any JavaScript validation code. Placeholder text disappears when typing starts, so never use it instead of a label—use aria-describedby to link visible hint text for screen reader users.",
"diagram": "Constraint Validation Rules\n\nAttribute Behaviors:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nminlength=\"8\" Validates on submit\n Can type less, can't submit\n Error: \"Too short (min 8)\"\n\nmaxlength=\"20\" Prevents typing\n Keyboard blocked at char 20\n No error (can't violate)\n\npattern=\"...\" Regex validation\n Example: \"[A-Z][a-z]+\"\n Error: \"Match format\"\n\nmin=\"1\" max=\"5\" Number range\n For type=\"number\"\n Error: \"Out of range\"\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nAccessibility Pattern:\n┌────────────────────────────┐\n│ <label for=\"pw\">Password │ ← Visible label\n│ <input id=\"pw\" │\n│ aria-describedby=\"hint\">│ ← Links to hint\n│ <small id=\"hint\"> │ ← Visible hint\n│ Must be 8-20 chars │ (not placeholder)\n└────────────────────────────┘"
},
"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",
"concept": {
"explanation": "This form demonstrates layered validation and accessibility best practices working together. Semantic input types (email, password) provide baseline validation and appropriate mobile keyboards, required attributes enforce mandatory fields, minlength adds password security rules, and the visual asterisks (*) in labels give sighted users a hint—but screen readers rely on the required attribute to announce \"Email, required, edit text\". Checkboxes can be required too, forcing users to agree to terms before submission. All validation happens natively in the browser before any server-side processing, saving bandwidth and providing instant feedback.",
"diagram": "Complete Form Validation Layers\n\n┌─────────────────────────────┐\n│ <input type=\"email\" │ Layer 1: Type Validation\n│ required │ Layer 2: Required\n│ minlength=\"8\"> │ Layer 3: Constraints\n└─────────────────────────────┘\n\nValidation Execution Order:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n1. Check required (not empty?)\n2. Check type (valid email?)\n3. Check constraints (minlength?)\n4. Check pattern (regex match?)\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nALL must pass to submit ✓\n\nAccessibility Checklist:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n✓ <label for=\"id\"> linked\n✓ Semantic input types\n✓ required attribute (not just *)\n✓ Visible error hints\n✓ Focus outline (keyboard nav)\n✓ Checkbox labeled correctly\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nBenefits:\n• No JavaScript needed\n• Localized error messages\n• Mobile keyboard optimization\n• Screen reader compatible\n• Instant client-side feedback"
},
"validations": [
{
"type": "attribute_value",
"value": { "selector": "#fullname", "attr": "required", "value": true },
"message": "Make the full name field <kbd>required</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#email", "attr": "type", "value": "email" },
"message": "Set the email input <kbd>type=\"email\"</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#email", "attr": "required", "value": true },
"message": "Make the email field <kbd>required</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "type", "value": "password" },
"message": "Set the password input <kbd>type=\"password\"</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "required", "value": true },
"message": "Make the password field <kbd>required</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "#password", "attr": "minlength", "value": "8" },
"message": "Add <kbd>minlength</kbd>=\"8\" to password"
},
{
"type": "attribute_value",
"value": { "selector": "#terms", "attr": "required", "value": true },
"message": "Make the terms checkbox <kbd>required</kbd>"
}
]
}

View File

@@ -17,6 +17,10 @@
"initialCode": "",
"solution": "<details>\n <summary>Click to reveal</summary>\n <p>This content was hidden!</p>\n</details>",
"previewContainer": "preview-area",
"concept": {
"explanation": "The details element is a native disclosure widget built into HTML5, meaning the browser handles all the show/hide logic without requiring JavaScript or CSS. When clicked, the browser toggles an internal 'open' state and applies native styling (like the disclosure triangle). This makes it accessible by default—screen readers announce it as an expandable section, and keyboard users can activate it with Enter or Space. The summary acts as a button that controls visibility of all sibling content inside the details element.",
"diagram": "Details/Summary Structure\n\n┌─────────────────────────────┐\n│ <details> │ ← Container\n│ ▸ <summary>Question</summary> │ ← Clickable toggle\n│ <!-- Hidden content --> │ (Browser renders ▸/▾)\n│ <p>Answer text...</p> │\n│ </details> │\n└─────────────────────────────┘\n\nClosed State (default):\n▸ Question\n\nOpen State:\n▾ Question\nAnswer text...\n\nBrowser Responsibilities:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n✓ Click handling\n✓ Keyboard support (Enter/Space)\n✓ Toggle arrow rendering\n✓ Screen reader announcements\n✓ Content show/hide\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
},
"validations": [
{
"type": "element_exists",
@@ -51,6 +55,10 @@
"initialCode": "<details>\n <summary>FAQ: What is HTML5?</summary>\n <p>HTML5 is the latest version of the HTML standard with new semantic elements and APIs.</p>\n</details>",
"solution": "<details open>\n <summary>FAQ: What is HTML5?</summary>\n <p>HTML5 is the latest version of the HTML standard with new semantic elements and APIs.</p>\n</details>",
"previewContainer": "preview-area",
"concept": {
"explanation": "Boolean attributes in HTML represent on/off states where presence equals true. The 'open' attribute tells the browser to render the details element in its expanded state on page load, but users can still collapse it by clicking. This is different from CSS display control—removing the attribute doesn't hide the element entirely, it just sets the initial collapsed state. JavaScript can dynamically add/remove this attribute to programmatically control the disclosure state, which fires a 'toggle' event that developers can listen to.",
"diagram": "Boolean Attribute Behavior\n\nHTML Boolean Attributes:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n<details open> → Expanded\n<details> → Collapsed\n<input required> → Must be filled\n<input> → Optional\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nThe 'open' Attribute:\n\n┌─────────────────────────────┐\n│ <details open> │ ← Attribute present\n│ ▾ Summary │ = Show content\n│ Content visible │\n│ </details> │\n└─────────────────────────────┘\n\n┌─────────────────────────────┐\n│ <details> │ ← Attribute absent\n│ ▸ Summary │ = Hide content\n│ </details> │\n└─────────────────────────────┘\n\nDynamic Control:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nJS: element.open = true → Expand\nJS: element.open = false → Collapse\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
},
"validations": [
{
"type": "attribute_value",
@@ -70,6 +78,10 @@
"initialCode": "",
"solution": "<h1>Frequently Asked Questions</h1>\n\n<details>\n <summary>What is HTML5?</summary>\n <p>HTML5 is the latest version of HTML with new semantic elements and APIs.</p>\n</details>\n\n<details>\n <summary>Do I need JavaScript?</summary>\n <p>Many interactive features work with pure HTML5, no JavaScript required!</p>\n</details>\n\n<details>\n <summary>Is this accessible?</summary>\n <p>Yes! Native HTML elements have built-in keyboard and screen reader support.</p>\n</details>",
"previewContainer": "preview-area",
"concept": {
"explanation": "Unlike many JavaScript accordion libraries that only allow one open panel at a time, native details elements are independent by default—users can open as many as they want simultaneously. This behavior is more user-friendly because it doesn't force users to lose their place when exploring multiple topics. Each details element maintains its own open/closed state in the DOM, so users can bookmark or reload the page and the browser may restore the state. The lack of mutual exclusivity is a feature, not a bug—if you need exclusive accordion behavior, you must add JavaScript to close siblings when one opens.",
"diagram": "Independent Accordion Pattern\n\nTraditional JS Accordion (Exclusive):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n▸ Question 1\n▾ Question 2 ← Only one can be\n Answer... open at a time\n▸ Question 3\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nNative Details (Independent):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n▾ Question 1 ← Multiple can be\n Answer... open at once\n▾ Question 2 (User choice!)\n Answer...\n▸ Question 3\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nBenefits:\n✓ Compare multiple answers\n✓ Print all expanded content\n✓ No state management complexity\n✓ Browser handles persistence\n\nTo Make Exclusive:\nAdd JS to listen for 'toggle' event\nand close siblings when one opens"
},
"validations": [
{
"type": "element_exists",

View File

@@ -17,6 +17,10 @@
"initialCode": "",
"solution": "<label for=\"download\">Download:</label>\n<progress id=\"download\" value=\"70\" max=\"100\">70%</progress>",
"previewContainer": "preview-area",
"concept": {
"explanation": "The progress element represents completion of a task with a known duration or endpoint—the browser calculates the fill percentage by dividing value by max. Browsers render progress bars with native OS styling by default, which means they look different on Windows, macOS, iOS, and Android, giving each platform's users a familiar appearance. Screen readers announce the completion percentage automatically (\"70 percent\"), and the element has an implicit ARIA role of 'progressbar'. JavaScript can update the value attribute dynamically to reflect real-time progress, and the text content inside serves as fallback for browsers that don't support progress.",
"diagram": "Progress Element Calculation\n\nFormula:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nFill % = (value / max) × 100\n\nExample:\n<progress value=\"70\" max=\"100\">\n\n 70 ÷ 100 = 0.7 → 70% filled\n\n┌─────────────────────────────┐\n│ Download: │\n├─────────────────────────────┤\n│ ████████████████░░░░░░░░░░ │ 70%\n└─────────────────────────────┘\n ← 70 units → ← 30 units →\n\nUpdating Progress:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nJS: element.value = 50 → 50%\nJS: element.value = 100 → 100%\n\nAccessibility:\nScreen reader announces:\n\"Download, progress bar, 70 percent\"\n\nFallback Content:\n<progress value=\"70\" max=\"100\">\n 70% ← Shown in old browsers\n</progress>"
},
"validations": [
{
"type": "element_exists",
@@ -51,6 +55,10 @@
"initialCode": "",
"solution": "<p>Loading...</p>\n<progress></progress>",
"previewContainer": "preview-area",
"concept": {
"explanation": "Indeterminate state communicates \"something is happening, but we don't know when it will finish\"—browsers render this with an animated pattern that moves continuously to show activity without implying a specific completion percentage. The animation style is platform-native: macOS uses a barber-pole stripe pattern, Windows uses a pulsing dot animation, and browsers may customize the appearance. This semantic distinction matters because it sets correct user expectations—a filled bar implies \"almost done\" while an animated loop implies \"still working\". Screen readers announce indeterminate progress as \"progress bar, busy\" rather than announcing a percentage.",
"diagram": "Determinate vs Indeterminate\n\nDeterminate (value present):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n<progress value=\"70\" max=\"100\">\n┌─────────────────────────────┐\n│ ████████████████░░░░░░░░░░ │ 70%\n└─────────────────────────────┘\n ↑ Known progress\n\nIndeterminate (no value):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n<progress></progress>\n┌─────────────────────────────┐\n│ ░░▓▓▓░░░░░░░░░░░░░░░░░░░░░ │ Animating\n└─────────────────────────────┘\n ↑ Unknown duration\n\nBrowser Animations:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nmacOS → Barber-pole stripes\nWindows → Pulsing dots\nAndroid → Circular spinner\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nUse Cases:\n✓ Network requests\n✓ File processing\n✓ Background sync\n✓ Unknown wait time\n\nScreen Reader:\n\"Progress bar, busy\""
},
"validations": [
{
"type": "element_exists",
@@ -75,6 +83,10 @@
"initialCode": "",
"solution": "<label for=\"battery\">Battery:</label>\n<meter id=\"battery\" value=\"0.8\" min=\"0\" max=\"1\" low=\"0.2\" high=\"0.8\" optimum=\"1\">80%</meter>",
"previewContainer": "preview-area",
"concept": {
"explanation": "Unlike progress (which shows task completion moving toward 100%), meter represents a measurement at a point in time that can be good or bad depending on context. The browser uses low/high/optimum thresholds to automatically color-code the gauge: green when value is near optimum, yellow in the middle range, and red when critically low or high. For example, battery at 80% is green (good), 40% is yellow (warning), and 10% is red (critical). This semantic intelligence means you don't need CSS—the browser applies appropriate colors based on your threshold values and whether higher or lower is better.",
"diagram": "Meter Threshold Logic\n\nAttributes:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nmin=\"0\" → Range start\nmax=\"1\" → Range end\nlow=\"0.2\" → Below this = bad\nhigh=\"0.8\" → Above this = depends\noptimum=\"1\" → Ideal value\nvalue=\"0.8\" → Current measurement\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nAutomatic Color Zones:\n\nBattery (optimum=high):\n┌─────────────────────────────┐\n│ 0.0 ────────────────── 1.0 │\n│ RED YELLOW GREEN │\n│ 0─0.2 0.2─0.8 0.8─1.0 │\n└─────────────────────────────┘\n ↑ value=0.8 (green)\n\nDisk Usage (optimum=low):\n┌─────────────────────────────┐\n│ 0% ─────────────────── 100% │\n│ GREEN YELLOW RED │\n│ 0─20 20─80 80─100 │\n└─────────────────────────────┘\n ↑ 90% (red)\n\nvs Progress:\nmeter → Snapshot measurement\nprogress → Task moving to 100%"
},
"validations": [
{
"type": "element_exists",

View File

@@ -17,6 +17,10 @@
"initialCode": "",
"solution": "<label for=\"browser\">Browser:</label>\n<input type=\"text\" id=\"browser\" list=\"browsers\">\n<datalist id=\"browsers\">\n <option value=\"Chrome\">\n <option value=\"Firefox\">\n <option value=\"Safari\">\n</datalist>",
"previewContainer": "preview-area",
"concept": {
"explanation": "Datalist provides native autocomplete without requiring JavaScript—the browser handles all the filtering, dropdown rendering, and selection logic. When users type, the browser automatically shows matching options using substring matching (typing \"fir\" shows \"Firefox\"). Unlike a select element that restricts users to predefined choices, datalist is advisory-only: users can ignore all suggestions and enter custom text, making it perfect for \"common values but allow anything\" scenarios. The dropdown styling is platform-native and keyboard-accessible (arrow keys to navigate, Enter to select), with full screen reader support announcing \"combobox with X suggestions\".",
"diagram": "Datalist vs Select\n\nDatalist (Flexible):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n<input list=\"browsers\">\n<datalist id=\"browsers\">\n <option value=\"Chrome\">\n <option value=\"Firefox\">\n</datalist>\n\n┌─────────────────────────────┐\n│ Fir_ │ ← User types\n├─────────────────────────────┤\n│ ▾ Suggestions: │\n│ Firefox │ ← Filtered\n└─────────────────────────────┘\n✓ Can type \"Brave\" (not listed)\n✓ Suggestions are optional\n\nSelect (Restricted):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n<select>\n <option>Chrome</option>\n <option>Firefox</option>\n</select>\n\n┌─────────────────────────────┐\n│ Chrome ▾ │\n├─────────────────────────────┤\n│ Chrome │\n│ Firefox │\n└─────────────────────────────┘\n✗ Can't type custom value\n✗ Must pick from list\n\nBrowser Handles:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n✓ Substring filtering\n✓ Dropdown rendering\n✓ Keyboard navigation\n✓ Screen reader announcements"
},
"validations": [
{
"type": "element_exists",
@@ -51,6 +55,10 @@
"initialCode": "",
"solution": "<label for=\"country\">Country:</label>\n<input type=\"text\" id=\"country\" list=\"countries\" placeholder=\"Start typing...\">\n<datalist id=\"countries\">\n <option value=\"Germany\">\n <option value=\"France\">\n <option value=\"Spain\">\n <option value=\"Italy\">\n</datalist>",
"previewContainer": "preview-area",
"concept": {
"explanation": "Datalists scale exceptionally well for long lists because filtering happens client-side without network requests—the browser holds all options in memory and filters instantly as the user types. This makes it ideal for common datasets like countries, states, or product names where you have hundreds of options but don't want to overwhelm users with a massive dropdown. Unlike select elements that require scrolling through all options, datalist progressively narrows results: typing \"ger\" in a 200-country list instantly shows only \"Germany\", \"Algeria\", and \"Niger\". This progressive disclosure pattern improves usability while maintaining full keyboard and screen reader accessibility.",
"diagram": "Progressive Filtering\n\nInitial State:\n┌─────────────────────────────┐\n│ Country: _ │\n└─────────────────────────────┘\n(No dropdown shown)\n\nUser types \"G\":\n┌─────────────────────────────┐\n│ Country: G_ │\n├─────────────────────────────┤\n│ ▾ Suggestions: │\n│ Germany │\n│ Georgia │\n│ Ghana │\n│ Greece │\n└─────────────────────────────┘\n\nUser types \"Ge\":\n┌─────────────────────────────┐\n│ Country: Ge_ │\n├─────────────────────────────┤\n│ ▾ Suggestions: │\n│ Germany │ ← Narrowed to 2\n│ Georgia │\n└─────────────────────────────┘\n\nUser types \"Ger\":\n┌─────────────────────────────┐\n│ Country: Ger_ │\n├─────────────────────────────┤\n│ ▾ Suggestions: │\n│ Germany │ ← Only 1 match\n└─────────────────────────────┘\n\nPerformance:\n✓ No network requests\n✓ Instant client-side filtering\n✓ Scales to hundreds of options\n✓ No JavaScript required"
},
"validations": [
{
"type": "element_exists",

View File

@@ -17,6 +17,10 @@
"initialCode": "",
"solution": "<article data-category=\"electronics\" data-price=\"299\">\n <h2>Laptop</h2>\n <p>A powerful laptop for work and play.</p>\n</article>\n\n<article data-category=\"clothing\" data-price=\"49\">\n <h2>T-Shirt</h2>\n <p>A comfortable cotton t-shirt.</p>\n</article>",
"previewContainer": "preview-area",
"concept": {
"explanation": "Data attributes provide a standards-compliant way to embed custom metadata directly in HTML without inventing non-standard attributes or abusing existing ones like class or id. The browser ignores data-* attributes for rendering but preserves them in the DOM, making them accessible to JavaScript and CSS. Unlike storing data in JavaScript variables or hidden divs, data attributes keep information with the element it describes, improving maintainability. JavaScript can read them via element.dataset (data-category becomes dataset.category), and CSS can select or display them using attribute selectors and attr(). This pattern separates presentation (CSS classes) from data (data attributes), following the principle of separation of concerns.",
"diagram": "Data Attribute Access\n\nHTML:\n<article data-category=\"electronics\"\n data-price=\"299\"\n data-in-stock=\"true\">\n\nJavaScript Access:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nconst el = document.querySelector('article');\nel.dataset.category → \"electronics\"\nel.dataset.price → \"299\"\nel.dataset.inStock → \"true\"\n\n(Hyphens become camelCase)\ndata-in-stock → dataset.inStock\n\nCSS Access:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n/* Select by attribute */\narticle[data-category=\"electronics\"] {\n border-color: blue;\n}\n\n/* Display value */\narticle::after {\n content: \"€\" attr(data-price);\n}\n\nvs Other Approaches:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n✗ class=\"electronics price-299\"\n → Mixes presentation & data\n✗ <div id=\"data-299\">\n → Abuses id attribute\n✓ data-category=\"electronics\"\n → Semantic & maintainable"
},
"validations": [
{
"type": "element_count",
@@ -46,6 +50,10 @@
"initialCode": "<ul>\n \n</ul>",
"solution": "<ul>\n <li data-status=\"completed\">Buy groceries</li>\n <li data-status=\"active\">Finish homework</li>\n <li data-status=\"pending\">Call mom</li>\n</ul>",
"previewContainer": "preview-area",
"concept": {
"explanation": "CSS attribute selectors enable state-based styling without adding/removing classes via JavaScript—you just change the attribute value and CSS reactivity handles the rest. The selector [data-status='active'] has the same specificity as a class (0,0,1,0), making it equally powerful but more semantic for data-driven states. This pattern shines in component libraries and SPAs where state changes frequently: updating one attribute triggers CSS transitions and visual changes automatically. Unlike classes that describe presentation (\"button-blue\"), data attributes describe meaning (\"status=active\"), and CSS translates meaning to presentation, keeping your HTML semantic and your styling decoupled from implementation details.",
"diagram": "CSS Attribute Selectors\n\nAttribute Selector Syntax:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n[data-status] → Has attribute\n[data-status=\"active\"] → Exact match\n[data-status^=\"act\"] → Starts with\n[data-status$=\"ed\"] → Ends with\n[data-status*=\"iv\"] → Contains\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nState-Based Styling:\n\n<li data-status=\"pending\">\n ↓ CSS applies\nli[data-status=\"pending\"] {\n background: lightblue;\n}\n\nJS changes state:\nel.dataset.status = \"active\"\n ↓ CSS automatically updates\nli[data-status=\"active\"] {\n background: orange;\n}\n\nvs Class Approach:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\ndata-status=\"active\"\n✓ Semantic (describes state)\n✓ One attribute to update\n✓ Clear data meaning\n\nclass=\"task active pending\"\n✗ Presentational\n✗ Multiple classes to manage\n✗ Unclear which is data\n\nSpecificity:\n[data-status=\"active\"] = .active\nBoth have 0,0,1,0 specificity"
},
"validations": [
{
"type": "element_exists",

View File

@@ -17,6 +17,10 @@
"initialCode": "",
"solution": "<dialog open>\n <h2>Welcome!</h2>\n <p>This is a native HTML dialog element.</p>\n <form method=\"dialog\">\n <button>Close</button>\n </form>\n</dialog>",
"previewContainer": "preview-area",
"concept": {
"explanation": "The dialog element is a native modal/popup that the browser manages entirely—it handles backdrop rendering, focus trapping, Escape key closing, and scroll locking on the body without any JavaScript. When opened with showModal(), the browser creates an ::backdrop pseudo-element (styled by CSS), traps keyboard focus inside the dialog (Tab cycles through dialog elements only), and prevents interaction with background content. The form method=\"dialog\" pattern leverages native form submission to close the dialog: any button inside submits the form, which closes the dialog and returns the button's value via the dialog's returnValue property. This replaces thousands of lines of modal library code with semantic HTML.",
"diagram": "Dialog Mechanics\n\nNative Modal Features:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n✓ Backdrop rendering\n✓ Focus trapping (Tab loops)\n✓ Escape key closes\n✓ Body scroll lock\n✓ Top-layer rendering\n✓ Screen reader isolation\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nDialog Opening Methods:\n\nshowModal() → Modal dialog\n ↓\n ┌─────────────────────┐\n │ [Backdrop overlay] │\n │ ┌───────────────┐ │\n │ │ <dialog> │ │\n │ │ Content │ │\n │ └───────────────┘ │\n └─────────────────────┘\n Focus trapped, Esc closes\n\nshow() → Non-modal dialog\n ↓\n ┌───────────────┐\n │ <dialog> │\n │ Content │ ← Floats above\n └───────────────┘\n Can interact with background\n\nForm Method=\"dialog\":\n<form method=\"dialog\">\n <button value=\"cancel\">Cancel</button>\n <button value=\"ok\">OK</button>\n</form>\n ↓ Clicking either button\n 1. Submits form\n 2. Closes dialog\n 3. Sets dialog.returnValue"
},
"validations": [
{
"type": "element_exists",
@@ -56,6 +60,10 @@
"initialCode": "",
"solution": "<dialog open>\n <h2>Confirm Delete</h2>\n <p>Are you sure you want to delete this item?</p>\n <form method=\"dialog\">\n <button value=\"cancel\">Cancel</button>\n <button value=\"delete\">Delete</button>\n </form>\n</dialog>",
"previewContainer": "preview-area",
"concept": {
"explanation": "The combination of dialog + form method=\"dialog\" creates a confirmation pattern where button values become the dialog's return value, letting you distinguish which button was clicked. When a user clicks a button in a method=\"dialog\" form, three things happen atomically: the form submits (triggering submit event), the dialog closes (triggering close event), and dialog.returnValue is set to the clicked button's value attribute. This pattern is perfect for yes/no confirmations or multi-choice prompts where you need to know the user's decision. Unlike window.confirm() which blocks the entire page and looks dated, dialog provides a customizable, non-blocking, accessible alternative that fits modern design systems.",
"diagram": "Dialog Return Values\n\nHTML:\n<dialog id=\"confirm\">\n <form method=\"dialog\">\n <button value=\"cancel\">Cancel</button>\n <button value=\"delete\">Delete</button>\n </form>\n</dialog>\n\nExecution Flow:\n\nUser clicks \"Delete\" button\n ↓\n1. Form submits\n (submit event fires)\n ↓\n2. Dialog closes\n (close event fires)\n ↓\n3. returnValue set\n dialog.returnValue = \"delete\"\n\nJavaScript Usage:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nconst dialog = document.querySelector('#confirm');\ndialog.showModal();\n\ndialog.addEventListener('close', () => {\n if (dialog.returnValue === 'delete') {\n // User confirmed\n deleteItem();\n } else {\n // User cancelled\n }\n});\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nvs window.confirm():\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nwindow.confirm() → Blocks page\n → Ugly native UI\n → No customization\n\n<dialog> → Non-blocking\n → Styleable\n → Accessible"
},
"validations": [
{
"type": "element_exists",

View File

@@ -17,6 +17,10 @@
"initialCode": "",
"solution": "<form>\n <fieldset>\n <legend>Personal Info</legend>\n <label for=\"name\">Name:</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n </fieldset>\n</form>",
"previewContainer": "preview-area",
"concept": {
"explanation": "Fieldset creates a semantic grouping that browsers and assistive technology understand as related form controls, not just a visual border. When a screen reader enters a fieldset, it announces the legend before each control (\"Personal Info, Name, edit text\"), providing context without repetition in every label. The browser also establishes a form control context: disabling the fieldset (disabled attribute) automatically disables all inputs inside, and form validation can be scoped to fieldsets. This semantic structure is crucial for complex forms—it transforms a flat list of inputs into a hierarchical document with clear relationships, improving both accessibility and maintainability.",
"diagram": "Fieldset Semantic Structure\n\nHTML:\n<form>\n <fieldset>\n <legend>Personal Info</legend>\n <label>Name: <input></label>\n <label>Email: <input></label>\n </fieldset>\n</form>\n\nScreen Reader Experience:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\"Personal Info, group\"\n → Focus first input\n \"Personal Info, Name, edit text\"\n → Tab to next input\n \"Personal Info, Email, edit text\"\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nContext announced once, then reused\n\nDisabled Propagation:\n\n<fieldset disabled>\n <input> ← Automatically disabled\n <input> ← Automatically disabled\n</fieldset>\n\nvs Individual Disabling:\n<input disabled>\n<input disabled> ← Must repeat\n\nForm Structure:\n\nFlat (no grouping):\n✗ Name\n✗ Email\n✗ Street\n✗ City\n\nGrouped (semantic):\n✓ Personal Info\n ✓ Name\n ✓ Email\n✓ Address\n ✓ Street\n ✓ City"
},
"validations": [
{
"type": "element_exists",
@@ -56,6 +60,10 @@
"initialCode": "",
"solution": "<form>\n <fieldset>\n <legend>Contact Us</legend>\n <label for=\"email\">Email:</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n <label for=\"message\">Message:</label>\n <textarea id=\"message\" name=\"message\" rows=\"4\"></textarea>\n <button type=\"submit\">Send Message</button>\n </fieldset>\n</form>",
"previewContainer": "preview-area",
"concept": {
"explanation": "The textarea element is designed for multi-line text input, automatically providing scroll bars when content exceeds its dimensions and preserving line breaks and whitespace on submission. Unlike input elements which ignore Enter key (using it for form submission), textarea captures Enter as a newline character, making it suitable for addresses, comments, messages, or any free-form text. The rows and cols attributes set initial dimensions as character counts (rows for lines, cols for width in characters), but CSS width/height override these. Textarea is a container element (not self-closing), so you must use &lt;textarea&gt;content&lt;/textarea&gt; syntax—any text between the tags becomes the initial value, preserving formatting.",
"diagram": "Textarea vs Input\n\nInput (single-line):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n<input type=\"text\" value=\"Hello\">\n\n┌─────────────────────────────┐\n│ Hello_ │\n└─────────────────────────────┘\n✗ Enter → Submits form\n✗ No line breaks\n✗ Self-closing tag\n\nTextarea (multi-line):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n<textarea rows=\"3\">\nHello\nWorld\n</textarea>\n\n┌─────────────────────────────┐\n│ Hello │\n│ World_ │\n│ │ ← rows=\"3\"\n└─────────────────────────────┘\n✓ Enter → New line\n✓ Preserves line breaks\n✓ Container element\n✓ Auto-scrolls if overflowing\n\nSizing:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nrows=\"4\" → 4 lines tall\ncols=\"40\" → 40 chars wide\nCSS overrides → width/height\n\nValue Syntax:\n<textarea>Initial text</textarea>\nvs\n<input value=\"Initial text\">"
},
"validations": [
{
"type": "element_exists",
@@ -95,6 +103,10 @@
"initialCode": "",
"solution": "<form>\n <fieldset>\n <legend>Account Info</legend>\n <label for=\"username\">Username:</label>\n <input type=\"text\" id=\"username\" name=\"username\">\n <label for=\"password\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\">\n </fieldset>\n <fieldset>\n <legend>Preferences</legend>\n <label for=\"bio\">Bio:</label>\n <textarea id=\"bio\" name=\"bio\"></textarea>\n </fieldset>\n <button type=\"submit\">Register</button>\n</form>",
"previewContainer": "preview-area",
"concept": {
"explanation": "Multiple fieldsets divide long forms into logical sections, improving cognitive load by chunking related fields together—users process \"fill out personal info\" and \"fill out address\" as distinct mental tasks rather than one overwhelming list. This pattern also enables progressive disclosure: you can hide/show fieldsets as wizard steps, disable future sections until current ones validate, or use CSS to style sections differently based on state. Screen readers announce fieldset boundaries (\"entering Personal Info group\", \"leaving Personal Info group\"), helping users maintain their place in complex forms. The semantic structure also aids form analytics: you can track which sections users struggle with or abandon most frequently.",
"diagram": "Multi-Fieldset Forms\n\nSingle Long Form:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n┌─────────────────────────────┐\n│ Name: [ ] │\n│ Email: [ ] │\n│ Username: [ ] │ Overwhelming\n│ Password: [ ] │ 8 fields at once\n│ Street: [ ] │\n│ City: [ ] │\n│ State: [ ] │\n│ Bio: [ ] │\n│ [Submit] │\n└─────────────────────────────┘\n\nChunked with Fieldsets:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n┌─────────────────────────────┐\n│ Personal Info │ Section 1\n│ Name: [ ] │ (2 fields)\n│ Email: [ ] │\n├─────────────────────────────┤\n│ Account │ Section 2\n│ Username: [ ] │ (2 fields)\n│ Password: [ ] │\n├─────────────────────────────┤\n│ Address │ Section 3\n│ Street: [ ] │ (3 fields)\n│ City: [ ] │\n│ State: [ ] │\n├─────────────────────────────┤\n│ About You │ Section 4\n│ Bio: [ ] │ (1 field)\n├─────────────────────────────┤\n│ [Submit] │\n└─────────────────────────────┘\n\nBenefits:\n✓ Reduced cognitive load\n✓ Clear visual hierarchy\n✓ Section-level validation\n✓ Progressive disclosure\n✓ Better analytics"
},
"validations": [
{
"type": "element_count",

View File

@@ -17,6 +17,10 @@
"initialCode": "",
"solution": "<figure>\n <img src=\"https://picsum.photos/400/200\" alt=\"A beautiful landscape\">\n <figcaption>A beautiful mountain landscape at sunset.</figcaption>\n</figure>",
"previewContainer": "preview-area",
"concept": {
"explanation": "The figure element semantically marks self-contained content that's referenced from the main flow but could be moved elsewhere (like a sidebar or appendix) without losing meaning. Figcaption provides an accessible label that screen readers announce when encountering the figure, establishing a programmatic relationship between image and caption that's stronger than visual proximity alone. Unlike an img followed by a p, figure+figcaption creates an accessibility API relationship: AT announces \"figure\" when entering, reads the caption, then describes the image, giving users complete context. Search engines also parse this relationship, using figcaption content to understand image meaning for image search results and context-aware rankings.",
"diagram": "Figure Semantic Relationship\n\nRegular Image + Text:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n<img src=\"photo.jpg\" alt=\"Mountain\">\n<p>A beautiful mountain.</p>\n\n✗ No semantic link\n✗ SR: \"Mountain image\" then \"A beautiful mountain\"\n✗ Caption could apply to any nearby content\n\nFigure + Figcaption:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n<figure>\n <img src=\"photo.jpg\" alt=\"Mountain\">\n <figcaption>A beautiful mountain.</figcaption>\n</figure>\n\n✓ Semantic relationship\n✓ SR: \"Figure. Mountain image. A beautiful mountain.\"\n✓ Caption explicitly bound to image\n\nScreen Reader Flow:\n┌─────────────────────────────┐\n│ <figure> │ → \"Entering figure\"\n│ <img alt=\"Mountain\"> │ → \"Mountain, image\"\n│ <figcaption> │\n│ A beautiful mountain │ → \"A beautiful mountain\"\n│ </figcaption> │\n│ </figure> │ → \"Leaving figure\"\n└─────────────────────────────┘\n\nSEO Benefits:\n✓ Image-caption binding\n✓ Better image search results\n✓ Context for visually similar images"
},
"validations": [
{
"type": "element_exists",
@@ -46,6 +50,10 @@
"initialCode": "",
"solution": "<figure>\n <pre><code>function greet(name) {\n return `Hello, ${name}!`;\n}</code></pre>\n <figcaption>A simple greeting function in JavaScript</figcaption>\n</figure>",
"previewContainer": "preview-area",
"concept": {
"explanation": "Figure isn't limited to images—it's for any self-contained content like code samples, charts, diagrams, poems, or quotes. The semantic meaning is \"referenced content with a caption\", not \"photo with text\". When wrapping code in figure, you establish that this code block is an example being discussed, not inline code to execute. Screen readers announce \"figure\" before the code, signaling to users that this is illustrative content they may want to skip if they're scanning. The figcaption describes what the code does or why it's shown, helping users decide whether to read it in detail—crucial for technical documentation where code blocks can be numerous and lengthy.",
"diagram": "Figure Use Cases\n\nImages:\n<figure>\n <img src=\"chart.png\">\n <figcaption>Sales 2024</figcaption>\n</figure>\n\nCode Samples:\n<figure>\n <pre><code>function add() {}</code></pre>\n <figcaption>Addition function</figcaption>\n</figure>\n\nQuotes:\n<figure>\n <blockquote>To be or not to be</blockquote>\n <figcaption>— Shakespeare</figcaption>\n</figure>\n\nPoems:\n<figure>\n <p>Roses are red</p>\n <p>Violets are blue</p>\n <figcaption>— Anonymous</figcaption>\n</figure>\n\nDiagrams (SVG):\n<figure>\n <svg>...</svg>\n <figcaption>System architecture</figcaption>\n</figure>\n\nCommon Pattern:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n✓ Content that illustrates a point\n✓ Content referenced by main text\n✓ Content with a description/attribution\n✓ Self-contained units\n\n✗ Not for decorative images\n✗ Not for inline content\n✗ Not for UI elements"
},
"validations": [
{
"type": "element_exists",
@@ -80,6 +88,10 @@
"initialCode": "",
"solution": "<figure>\n <img src=\"https://picsum.photos/200/120?1\" alt=\"Photo 1\">\n <img src=\"https://picsum.photos/200/120?2\" alt=\"Photo 2\">\n <img src=\"https://picsum.photos/200/120?3\" alt=\"Photo 3\">\n <img src=\"https://picsum.photos/200/120?4\" alt=\"Photo 4\">\n <figcaption>My vacation photo gallery</figcaption>\n</figure>",
"previewContainer": "preview-area",
"concept": {
"explanation": "A single figure can contain multiple related elements when they collectively form one logical unit, like a photo gallery, before/after comparison, or multi-angle product shots. The figcaption describes the entire collection rather than individual items, establishing that these pieces should be understood together. This pattern is semantically different from multiple separate figures—it communicates \"these images are facets of one concept\" versus \"here are several independent illustrations\". Screen readers announce one figure containing multiple images, then read the collective caption, helping users understand the grouping. Search engines also interpret this structure, understanding that images within a figure are related for relevance ranking.",
"diagram": "Single vs Multiple Figures\n\nMultiple Separate Figures:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n<figure>\n <img src=\"paris.jpg\">\n <figcaption>Paris</figcaption>\n</figure>\n<figure>\n <img src=\"london.jpg\">\n <figcaption>London</figcaption>\n</figure>\n→ Two independent illustrations\n\nSingle Figure Gallery:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n<figure>\n <img src=\"paris.jpg\">\n <img src=\"london.jpg\">\n <img src=\"rome.jpg\">\n <figcaption>European cities</figcaption>\n</figure>\n→ One concept with multiple views\n\nUse Cases:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n✓ Photo gallery (vacation pics)\n✓ Before/after comparison\n✓ Product: front, side, back views\n✓ Step-by-step process images\n✓ Multi-panel comics/diagrams\n\nScreen Reader:\n\"Figure containing 4 images\"\n→ Image 1: \"paris.jpg\"\n→ Image 2: \"london.jpg\"\n→ Image 3: \"rome.jpg\"\n→ Image 4: \"berlin.jpg\"\n\"Caption: European cities\""
},
"validations": [
{
"type": "element_exists",

View File

@@ -2,21 +2,25 @@
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "html-tables",
"title": "HTML Tables",
"description": "Create structured data tables with semantic markup",
"description": "Create structured data tables with headers and captions",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "table-basic",
"title": "Data Tables",
"description": "Tables display structured data in rows and columns. Use <kbd>&lt;table&gt;</kbd> as the container, <kbd>&lt;tr&gt;</kbd> for rows, <kbd>&lt;th&gt;</kbd> for header cells, and <kbd>&lt;td&gt;</kbd> for data cells.<br><br>Add <kbd>&lt;caption&gt;</kbd> for an accessible title that describes the table's content.",
"task": "Create a pricing table:<br>1. A <kbd>&lt;caption&gt;</kbd> saying <code>Pricing</code><br>2. A header row with <code>Plan</code> and <code>Price</code><br>3. Two data rows for Basic ($9) and Pro ($29)",
"title": "Basic Table Structure",
"description": "Tables use <kbd>&lt;table&gt;</kbd> with <kbd>&lt;tr&gt;</kbd> for rows. Inside rows, use <kbd>&lt;th&gt;</kbd> for headers and <kbd>&lt;td&gt;</kbd> for data cells.<br><br>The <kbd>&lt;caption&gt;</kbd> element provides an accessible title for the table.",
"task": "Create a simple table with:<br>1. A <kbd>&lt;caption&gt;</kbd> saying <code>Fruit Prices</code><br>2. A header row with <code>Fruit</code> and <code>Price</code> columns<br>3. At least 2 data rows",
"previewHTML": "",
"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; }",
"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; }",
"sandboxCSS": "",
"initialCode": "",
"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>",
"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>",
"previewContainer": "preview-area",
"concept": {
"explanation": "HTML tables communicate semantic data relationships through row/column structure, enabling screen readers to navigate two-dimensionally (announcing \"row 2, column 1: Apple\" or \"Fruit column, row 2\") instead of linearly reading cells. The th (header cell) vs td (data cell) distinction creates accessibility associations: screen readers remember headers and announce them when navigating data cells, giving context. Caption provides a programmatic table title that screen readers announce before entering the table structure. The table element has implicit ARIA role=\"table\", and browsers expose table semantics through accessibility APIs, allowing AT users to jump between tables, skip table content, or navigate by row/column.",
"diagram": "Table Semantic Structure\n\n<table>\n <caption>Fruit Prices</caption>\n <tr> ← Row 1 (header)\n <th>Fruit</th> ← Column 1 header\n <th>Price</th> ← Column 2 header\n </tr>\n <tr> ← Row 2 (data)\n <td>Apple</td> ← Data cell\n <td>$1.50</td>\n </tr>\n</table>\n\nScreen Reader Navigation:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\"Table, Fruit Prices\"\n\"2 columns, 2 rows\"\n\nEnter table:\n\"Row 1, Column 1: Fruit, header\"\n→ Right arrow\n\"Row 1, Column 2: Price, header\"\n→ Down arrow\n\"Row 2, Column 2: $1.50\"\n(Still remembers \"Price\" header)\n\nHeader Association:\n┌─────────┬─────────┐\n│ Fruit │ Price │ ← th elements\n├─────────┼─────────┤\n│ Apple │ $1.50 │\n└─────────┴─────────┘\n ↑ ↑\n └─────────┘\n When SR focuses on\n \"$1.50\", it announces:\n \"Price: $1.50, row 2\"\n\nvs div Table:\n✗ No semantic structure\n✗ Linear reading only\n✗ No header association"
},
"validations": [
{
"type": "element_exists",
@@ -31,60 +35,103 @@
{
"type": "element_count",
"value": { "selector": "th", "min": 2 },
"message": "Add header cells (<kbd>&lt;th&gt;</kbd>) for Plan and Price"
"message": "Add at least 2 header cells (th)"
},
{
"type": "element_count",
"value": { "selector": "tr", "min": 3 },
"message": "Add 3 rows (1 header + 2 data rows)"
"message": "Add at least 3 rows (1 header + 2 data rows)"
}
]
},
{
"id": "table-sections",
"title": "Table Sections",
"description": "Semantic table sections improve accessibility and allow for separate styling:<br><br>• <kbd>&lt;thead&gt;</kbd> — header section<br>• <kbd>&lt;tbody&gt;</kbd> — main content<br>• <kbd>&lt;tfoot&gt;</kbd> footer (totals, summaries)",
"task": "Wrap the header row in <kbd>&lt;thead&gt;</kbd> and data rows in <kbd>&lt;tbody&gt;</kbd>.",
"id": "table-thead-tbody",
"title": "Table Head & Body",
"description": "Use <kbd>&lt;thead&gt;</kbd> to group header rows and <kbd>&lt;tbody&gt;</kbd> to group data rows. This helps browsers and assistive technology understand the table structure.<br><br>You can also use <kbd>&lt;tfoot&gt;</kbd> for footer rows like totals.",
"task": "Create a structured table:<br>1. A <kbd>&lt;caption&gt;</kbd> with <code>Monthly Sales</code><br>2. A <kbd>&lt;thead&gt;</kbd> with Month and Revenue headers<br>3. A <kbd>&lt;tbody&gt;</kbd> with at least 2 data rows",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } table { border-collapse: collapse; width: 100%; max-width: 350px; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } th, td { padding: 12px 16px; text-align: left; } thead { background: steelblue; color: white; } tbody tr:nth-child(even) { background: #f8f9fa; }",
"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": "<table>\n <tr>\n <th>Name</th>\n <th>Score</th>\n </tr>\n <tr>\n <td>Alice</td>\n <td>95</td>\n </tr>\n <tr>\n <td>Bob</td>\n <td>87</td>\n </tr>\n</table>",
"solution": "<table>\n <thead>\n <tr>\n <th>Name</th>\n <th>Score</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>Alice</td>\n <td>95</td>\n </tr>\n <tr>\n <td>Bob</td>\n <td>87</td>\n </tr>\n </tbody>\n</table>",
"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",
"concept": {
"explanation": "The thead/tbody/tfoot elements create logical sections that browsers can optimize for printing (repeating headers on each page), scrolling (sticky headers while tbody scrolls), and accessibility (screen readers announce section boundaries). This grouping also enables CSS to style sections differently without classes—tbody tr:hover works naturally. Some browsers display thead/tfoot with distinct styling by default. Screen readers announce section transitions (\"entering table header\", \"entering table body\") helping users understand where they are in large tables. For very long tables, browsers may keep thead fixed while scrolling tbody, and when printing multi-page tables, browsers repeat thead at the top of each printed page automatically.",
"diagram": "Table Section Structure\n\n<table>\n <caption>Sales Data</caption>\n <thead> ← Header section\n <tr><th>Month</th></tr>\n </thead>\n <tbody> ← Data section\n <tr><td>Jan</td></tr>\n <tr><td>Feb</td></tr>\n ...(many rows)\n </tbody>\n <tfoot> ← Footer section\n <tr><td>Total</td></tr>\n </tfoot>\n</table>\n\nPrinting Long Tables:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nPage 1:\n┌─────────────┐\n│ Month │ ← thead (repeated)\n├─────────────┤\n│ Jan │\n│ Feb │ tbody continues...\n\nPage 2:\n┌─────────────┐\n│ Month │ ← thead (repeated)\n├─────────────┤\n│ Mar │\n│ Apr │ tbody continues...\n\nScrolling Long Tables:\n┌─────────────────────────────┐\n│ Month │ Revenue │ Fixed │ ← thead sticky\n├───────────┴─────────┴───────┤\n│ Jan │ $10,000 │ ↕\n│ Feb │ $12,000 │ Scrolls\n│ Mar │ $11,500 │ ↕\n│ ... │\n└─────────────────────────────┘\n\nScreen Reader:\n\"Entering table header\"\n→ Reads headers\n\"Entering table body\"\n→ Reads data rows\n\"Entering table footer\"\n→ Reads totals"
},
"validations": [
{
"type": "element_exists",
"value": "table",
"message": "Add a <kbd>&lt;table&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "caption",
"message": "Add a <kbd>&lt;caption&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "thead",
"message": "Add a <kbd>&lt;thead&gt;</kbd> section for the header"
"message": "Add a <kbd>&lt;thead&gt;</kbd> for the header section"
},
{
"type": "element_exists",
"value": "tbody",
"message": "Add a <kbd>&lt;tbody&gt;</kbd> section for the data"
"message": "Add a <kbd>&lt;tbody&gt;</kbd> for the data rows"
},
{
"type": "element_count",
"value": { "selector": "tbody tr", "min": 2 },
"message": "Add at least 2 data rows in tbody"
}
]
},
{
"id": "table-colspan",
"title": "Spanning Columns",
"description": "The <kbd>colspan</kbd> attribute lets a cell span multiple columns. This is useful for headers that group multiple columns or footer totals.<br><br><pre>&lt;td colspan=\"2\"&gt;...&lt;/td&gt;</pre>",
"task": "Add a footer row that spans both columns using <kbd>colspan=\"2\"</kbd>.",
"id": "table-complete",
"title": "Complete Table with Footer",
"description": "Add <kbd>&lt;tfoot&gt;</kbd> to create a footer section for totals or summary data. The footer stays at the bottom even if tbody has many rows.<br><br>Combine all sections for a fully structured, accessible table.",
"task": "Create a complete table:<br>1. A <kbd>&lt;caption&gt;</kbd> with <code>Order Summary</code><br>2. A <kbd>&lt;thead&gt;</kbd> with Item and Price headers<br>3. A <kbd>&lt;tbody&gt;</kbd> with 2 items<br>4. A <kbd>&lt;tfoot&gt;</kbd> with a Total row",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } table { border-collapse: collapse; width: 100%; max-width: 350px; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #eee; } thead { background: steelblue; color: white; } tfoot { background: #f0f0f0; font-weight: 600; }",
"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": "<table>\n <thead>\n <tr>\n <th>Item</th>\n <th>Price</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>Coffee</td>\n <td>$4</td>\n </tr>\n <tr>\n <td>Cake</td>\n <td>$6</td>\n </tr>\n </tbody>\n</table>",
"solution": "<table>\n <thead>\n <tr>\n <th>Item</th>\n <th>Price</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>Coffee</td>\n <td>$4</td>\n </tr>\n <tr>\n <td>Cake</td>\n <td>$6</td>\n </tr>\n </tbody>\n <tfoot>\n <tr>\n <td colspan=\"2\">Total: $10</td>\n </tr>\n </tfoot>\n</table>",
"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",
"concept": {
"explanation": "The tfoot element defines summary or calculation rows that semantically belong at the table's end, even though in HTML source order it can appear before tbody (browsers render it at the bottom regardless). This location flexibility is historical—placing tfoot before tbody in source allows browsers to render footers before receiving all body data, useful for streaming large datasets. Screen readers announce tfoot as \"table footer\" when entering, signaling that this row contains aggregate data rather than individual records. Tfoot is ideal for totals, averages, counts, or any row that summarizes the data above—it gives these special rows semantic meaning that plain tbody rows lack.",
"diagram": "Tfoot Source Order Flexibility\n\nHTML Source Order (optional):\n<table>\n <thead>...</thead>\n <tfoot>...</tfoot> ← Before tbody\n <tbody>...</tbody>\n</table>\n\nBrowser Renders:\n┌─────────────────────────────┐\n│ thead (headers) │\n├─────────────────────────────┤\n│ tbody (data rows) │\n│ ... │\n├─────────────────────────────┤\n│ tfoot (totals) │ ← Rendered last\n└─────────────────────────────┘\n\nWhy Allow tfoot Before tbody?\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nStreaming Large Datasets:\n1. Send <thead>\n2. Send <tfoot>\n3. Stream <tbody> (may take time)\n→ Footer renders before all data\n\nSemantic Meaning:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n<tbody> → Individual records\n<tfoot> → Aggregate summary\n\nScreen Reader:\n\"Table footer, row 1\"\n\"Total, $60.00\"\n\nCommon tfoot Content:\n✓ Totals/Subtotals\n✓ Averages\n✓ Record counts\n✓ Summary calculations\n\n✗ Not for regular data rows\n✗ Not for pagination controls"
},
"validations": [
{
"type": "element_exists",
"value": "tfoot",
"message": "Add a <kbd>&lt;tfoot&gt;</kbd> section"
"value": "table",
"message": "Add a <kbd>&lt;table&gt;</kbd> element"
},
{
"type": "contains",
"value": "colspan",
"message": "Use <kbd>colspan</kbd> to span columns"
"type": "element_exists",
"value": "caption",
"message": "Add a <kbd>&lt;caption&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "thead",
"message": "Add a <kbd>&lt;thead&gt;</kbd> section"
},
{
"type": "element_exists",
"value": "tbody",
"message": "Add a <kbd>&lt;tbody&gt;</kbd> section"
},
{
"type": "element_exists",
"value": "tfoot",
"message": "Add a <kbd>&lt;tfoot&gt;</kbd> section for the total"
},
{
"type": "element_count",
"value": { "selector": "tbody tr", "min": 2 },
"message": "Add at least 2 item rows in tbody"
}
]
}

View File

@@ -17,6 +17,10 @@
"initialCode": "",
"solution": "<marquee>Welcome to my website!</marquee>",
"previewContainer": "preview-area",
"concept": {
"explanation": "Marquee is a non-standard HTML element introduced by Netscape in the 1990s that created auto-scrolling text without JavaScript—browsers handled animation entirely natively. While deprecated by W3C standards (never officially standardized), browsers still support it for backward compatibility with legacy websites. The element teaches an important web history lesson: browser vendors sometimes implement features unilaterally, and if popular enough, those features persist even after standards bodies reject them. Modern development uses CSS animations (`@keyframes` + `animation`) or JavaScript for scrolling effects, giving developers more control and adhering to web standards, but marquee demonstrates how declarative HTML can embed behavior.",
"diagram": "Marquee: A Web History Lesson\n\nTimeline:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n1995 → Netscape adds <marquee>\n1996 → IE copies it (vendor wars)\n2000s → W3C: \"This is non-standard\"\n2024 → Still works! (legacy compat)\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nHow It Works:\n<marquee>Text</marquee>\n ↓\nBrowser sees marquee element\n ↓\nNative animation engine starts\n ↓\nText scrolls without JS/CSS\n\nModern Equivalent:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n<div class=\"scroll\">Text</div>\n\n.scroll {\n animation: scroll 10s linear infinite;\n}\n\n@keyframes scroll {\n from { transform: translateX(100%); }\n to { transform: translateX(-100%); }\n}\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nWhy Deprecated?\n✗ Non-standard (vendor-specific)\n✗ Accessibility issues (motion)\n✗ Limited control\n✗ No standard API\n\n✓ Use CSS animations instead\n✓ Add prefers-reduced-motion\n✓ Full control over timing/easing"
},
"validations": [
{
"type": "element_exists",
@@ -36,6 +40,10 @@
"initialCode": "",
"solution": "<marquee behavior=\"alternate\">Bounce! Bounce! Bounce!</marquee>",
"previewContainer": "preview-area",
"concept": {
"explanation": "Marquee attributes control animation parameters through HTML rather than CSS or JavaScript—this declarative approach was innovative for the 1990s but inflexible by modern standards. The behavior attribute changes motion physics: 'scroll' creates continuous looping (exits one side, enters opposite), 'slide' stops when reaching the edge (one-time animation), and 'alternate' bounces back and forth (ping-pong effect). Direction controls axis (left/right for horizontal, up/down for vertical), and scrollamount sets pixels moved per frame (higher = faster). These attributes demonstrate early attempts at animation control before CSS animations existed, showing how HTML sometimes blurred the line between structure and presentation.",
"diagram": "Marquee Behaviors\n\nbehavior=\"scroll\" (default):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n→ Text → → → → → (loops)\n\nbehavior=\"slide\":\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n→ Text → → STOP\n(animates once, stops at edge)\n\nbehavior=\"alternate\":\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n→ Text → → ← ← Text ← ←\n(bounces back and forth)\n\nDirection:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\ndirection=\"left\" → Text moves ←\ndirection=\"right\" → Text moves →\ndirection=\"up\" → Text moves ↑\ndirection=\"down\" → Text moves ↓\n\nSpeed Control:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nscrollamount=\"1\" → Slow (1px/frame)\nscrollamount=\"6\" → Default\nscrollamount=\"20\" → Fast (20px/frame)\n\nCombinations:\n<marquee direction=\"right\" \n behavior=\"alternate\"\n scrollamount=\"10\">\n Bounces horizontally at 10px/frame\n</marquee>"
},
"validations": [
{
"type": "element_exists",
@@ -60,6 +68,10 @@
"initialCode": "",
"solution": "<marquee direction=\"left\" scrollamount=\"5\">BREAKING NEWS: Marquee element still works in browsers!</marquee>",
"previewContainer": "preview-area",
"concept": {
"explanation": "Marquee represents a cautionary tale about web standards versus implementation reality: despite being deprecated for over a decade and never formally standardized, browsers maintain support because removing it would break thousands of legacy websites. This teaches the web's core principle of \"don't break the web\"—backward compatibility trumps clean standards. Modern developers should avoid marquee for accessibility reasons (motion can trigger vestibular disorders, and it ignores prefers-reduced-motion), lack of control (can't pause on hover, sync with other animations, or adjust timing curves), and semantic incorrectness (mixing behavior into structure). Instead, CSS animations provide the same visual effects with full control, accessibility hooks, and standards compliance.",
"diagram": "Legacy Compat vs Modern Standards\n\nThe \"Don't Break the Web\" Principle:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n1995 → Site uses <marquee>\n2024 → Site still online (unchanged)\n → Browser must still support it\n → Can't remove deprecated feature\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nAccessibility Issues:\n✗ Triggers motion sickness\n✗ Ignores prefers-reduced-motion\n✗ Can't pause on hover\n✗ Distracts from content\n✗ Not keyboard-accessible\n\nModern Replacement:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nHTML:\n<div class=\"ticker\">BREAKING NEWS</div>\n\nCSS:\n@media (prefers-reduced-motion: no-preference) {\n .ticker {\n animation: scroll 10s linear infinite;\n }\n}\n\n@keyframes scroll {\n from { transform: translateX(100%); }\n to { transform: translateX(-100%); }\n}\n\n.ticker:hover {\n animation-play-state: paused;\n}\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nLesson Learned:\n✓ Standards matter\n✓ Accessibility first\n✓ Use CSS for presentation\n✓ Don't mix behavior into HTML\n✓ Respect user preferences"
},
"validations": [
{
"type": "element_exists",

View File

@@ -17,6 +17,10 @@
"initialCode": "",
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"steelblue\" />\n</svg>",
"previewContainer": "preview-area",
"concept": {
"explanation": "SVG (Scalable Vector Graphics) defines graphics mathematically rather than as pixels, meaning shapes stay crisp at any zoom level or screen resolution—unlike raster images (PNG, JPG) that pixelate when scaled. The browser renders SVG by calculating shape geometry at display time: a circle at (100,100) with radius 50 is recomputed for each pixel density (1x, 2x, 3x displays). This makes SVG perfect for icons, logos, charts, and responsive graphics. SVG elements are DOM nodes like HTML elements, so you can style them with CSS (fill, stroke), animate them with CSS animations or JavaScript, and attach event listeners. The coordinate system starts at top-left (0,0), with x increasing rightward and y increasing downward.",
"diagram": "Vector vs Raster Graphics\n\nRaster (PNG/JPG):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nStores pixels:\n100x100 image = 10,000 pixels\n\n1x display: ■■■■ (crisp)\n2x display: ▪▪▪▪ (pixelated)\n\nVector (SVG):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nStores math:\n<circle cx=\"100\" cy=\"100\" r=\"50\"/>\n\n1x display: ● (crisp)\n2x display: ● (still crisp!)\n\nSVG Coordinate System:\n(0,0) ─────────→ x\n │\n │ (100,100)\n │ ● ← center\n │\n ↓\n y\n\nCircle Attributes:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\ncx=\"100\" → Center X coordinate\ncy=\"100\" → Center Y coordinate\nr=\"50\" → Radius (pixels)\nfill=\"steelblue\" → Interior color\n\nBenefits:\n✓ Resolution-independent\n✓ Small file size\n✓ CSS styleable\n✓ Animatable\n✓ Accessible (text labels)\n✓ DOM-scriptable"
},
"validations": [
{
"type": "element_exists",
@@ -66,6 +70,10 @@
"initialCode": "",
"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",
"concept": {
"explanation": "SVG distinguishes between filled shapes (solid interior) and stroked shapes (outline only) through fill and stroke attributes. Rectangles use top-left corner positioning (x, y) plus dimensions (width, height), while lines use start point (x1, y1) and end point (x2, y2) coordinates—no implicit fill for lines since they're one-dimensional. Lines require an explicit stroke to be visible because fill doesn't apply to one-dimensional paths. Stroke-width controls line thickness in user units (typically pixels), and additional stroke properties like stroke-linecap (round, square, butt) and stroke-linejoin (round, bevel, miter) control how stroke endpoints and corners render. SVG's presentation attributes (fill, stroke) can be overridden by CSS for dynamic styling.",
"diagram": "SVG Shape Positioning\n\nRectangle Coordinates:\n(0,0)\n ┌─────────────────→ x\n │ (20,20)\n │ ┌────────┐ ← x=\"20\" y=\"20\"\n │ │ │ width=\"80\"\n │ │ rect │ height=\"60\"\n │ └────────┘\n ↓\n y\n\nLine Coordinates:\n(x1,y1) (x2,y2)\n ●───────────────●\n(120,30) (180,90)\n ↑ start ↑ end\n\nFill vs Stroke:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n<rect fill=\"red\"> ← Solid interior\n<rect fill=\"red\" stroke=\"black\" stroke-width=\"2\">\n ↑ interior ↑ outline\n\n<line stroke=\"blue\"> ← Only visible\n<line fill=\"red\"> ← Ignored!\n\nStroke Properties:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nstroke=\"color\"\nstroke-width=\"3\" → Thickness\nstroke-linecap=\"round\" → End shape\nstroke-dasharray=\"5,5\" → Dashed line\nstroke-opacity=\"0.5\" → Transparency\n\nPresentation Attributes:\nHTML: <rect fill=\"red\">\nCSS: rect { fill: red; }\nCSS wins if both present!"
},
"validations": [
{
"type": "element_exists",
@@ -150,6 +158,10 @@
"initialCode": "",
"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",
"concept": {
"explanation": "SVG elements stack in source order like HTML—later elements render on top of earlier ones, creating a painter's algorithm where each shape is \"painted\" over previous shapes. This z-order control means shape placement in your markup defines layering: the face circle must come before the eye circles for eyes to appear on top. Unlike CSS z-index (which requires positioning context), SVG stacking is purely document-order based. You can group related shapes with <g> elements for organizational purposes and apply transformations or styles to the entire group. SVG's declarative nature makes it ideal for programmatic generation: you can template SVG markup or use JavaScript to create/manipulate shapes dynamically, and the browser automatically handles rendering updates.",
"diagram": "SVG Stacking Order\n\nSource Order = Paint Order:\n<svg>\n <circle ... /> ← Painted first (back)\n <circle ... /> ← Painted second\n <circle ... /> ← Painted third\n <line ... /> ← Painted last (front)\n</svg>\n\nVisual Result:\n Layer 4 (front)\n │\n Layer 3\n │\n Layer 2\n │\n Layer 1 (back)\n\nSmiley Face Example:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n1. Face (large circle)\n2. Left eye (small circle) ─┐\n3. Right eye (small circle) ├→ On top of face\n4. Smile (line) ─┘\n\nGrouping with <g>:\n<g id=\"eyes\">\n <circle cx=\"70\" cy=\"80\" r=\"10\"/>\n <circle cx=\"130\" cy=\"80\" r=\"10\"/>\n</g>\n\nBenefits:\n✓ Apply transform to group\n✓ Style entire group\n✓ Semantic organization\n✓ Easy to show/hide\n\nDynamic SVG:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nJS: circle.setAttribute('r', 60);\n→ Browser re-renders instantly\n\nJS: svg.innerHTML += '<circle.../>';\n→ Add shapes dynamically\n\nvs Canvas:\nSVG = Retained mode (DOM)\nCanvas = Immediate mode (pixels)"
},
"validations": [
{
"type": "element_exists",

View File

@@ -1,88 +0,0 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "html-semantic",
"title": "Semantic HTML",
"mode": "html",
"description": "Use meaningful HTML elements to structure content properly.",
"difficulty": "beginner",
"lessons": [
{
"id": "semantic-1",
"title": "The <article> Element",
"description": "The <kbd>&lt;article&gt;</kbd> element represents self-contained content that could be distributed independently, like a blog post, news article, or comment.<br><br><pre>&lt;article&gt;\n &lt;h2&gt;Article Title&lt;/h2&gt;\n &lt;p&gt;Article content...&lt;/p&gt;\n&lt;/article&gt;</pre>",
"task": "Wrap the blog post content in an <kbd>&lt;article&gt;</kbd> element.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } article { padding: 1rem; background: #f9f9f9; border-left: 4px solid steelblue; border-radius: 4px; } h2 { margin: 0 0 8px; color: steelblue; } p { margin: 0; color: #555; line-height: 1.5; }",
"sandboxCSS": "",
"codePrefix": "",
"initialCode": "<h2>My First Post</h2>\n<p>This is a blog post about learning HTML.</p>",
"codeSuffix": "",
"solution": "<article>\n<h2>My First Post</h2>\n<p>This is a blog post about learning HTML.</p>\n</article>",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "<article>",
"message": "Add an opening <kbd>&lt;article&gt;</kbd> tag"
},
{
"type": "contains",
"value": "</article>",
"message": "Add a closing <kbd>&lt;/article&gt;</kbd> tag"
}
]
},
{
"id": "semantic-2",
"title": "The <section> Element",
"description": "The <kbd>&lt;section&gt;</kbd> element represents a thematic grouping of content, typically with a heading. Use it to divide a page into logical sections.<br><br><pre>&lt;section&gt;\n &lt;h2&gt;Features&lt;/h2&gt;\n &lt;p&gt;Our product features...&lt;/p&gt;\n&lt;/section&gt;</pre>",
"task": "Wrap the features content in a <kbd>&lt;section&gt;</kbd> element.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } section { padding: 1rem; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); border-radius: 8px; } h2 { margin: 0 0 12px; } ul { margin: 0; padding-left: 1.5rem; } li { margin: 4px 0; color: #444; }",
"sandboxCSS": "",
"codePrefix": "",
"initialCode": "<h2>Features</h2>\n<ul>\n <li>Fast performance</li>\n <li>Easy to use</li>\n</ul>",
"codeSuffix": "",
"solution": "<section>\n<h2>Features</h2>\n<ul>\n <li>Fast performance</li>\n <li>Easy to use</li>\n</ul>\n</section>",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "<section>",
"message": "Add an opening <kbd>&lt;section&gt;</kbd> tag"
},
{
"type": "contains",
"value": "</section>",
"message": "Add a closing <kbd>&lt;/section&gt;</kbd> tag"
}
]
},
{
"id": "semantic-3",
"title": "The <aside> Element",
"description": "The <kbd>&lt;aside&gt;</kbd> element represents content tangentially related to the main content, like sidebars, pull quotes, or related links.<br><br><pre>&lt;aside&gt;\n &lt;h3&gt;Related&lt;/h3&gt;\n &lt;ul&gt;...&lt;/ul&gt;\n&lt;/aside&gt;</pre>",
"task": "Wrap the related links in an <kbd>&lt;aside&gt;</kbd> element.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } aside { padding: 1rem; background: #fff8e7; border: 1px solid #ffe0a6; border-radius: 8px; } h3 { margin: 0 0 8px; color: #b8860b; font-size: 0.9rem; text-transform: uppercase; } ul { margin: 0; padding-left: 1.2rem; } li { margin: 4px 0; } a { color: #b8860b; }",
"sandboxCSS": "",
"codePrefix": "",
"initialCode": "<h3>Related Posts</h3>\n<ul>\n <li><a href=\"#\">CSS Basics</a></li>\n <li><a href=\"#\">HTML Tips</a></li>\n</ul>",
"codeSuffix": "",
"solution": "<aside>\n<h3>Related Posts</h3>\n<ul>\n <li><a href=\"#\">CSS Basics</a></li>\n <li><a href=\"#\">HTML Tips</a></li>\n</ul>\n</aside>",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "<aside>",
"message": "Add an opening <kbd>&lt;aside&gt;</kbd> tag"
},
{
"type": "contains",
"value": "</aside>",
"message": "Add a closing <kbd>&lt;/aside&gt;</kbd> tag"
}
]
}
]
}

View File

@@ -1,197 +0,0 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "markdown-basics",
"title": "Markdown Basics",
"description": "Learn to format text documents with Markdown, a simple and readable markup language used everywhere from GitHub to note-taking apps.",
"mode": "markdown",
"difficulty": "beginner",
"lessons": [
{
"id": "md-headings",
"title": "Headings",
"description": "Markdown uses hash symbols <kbd>#</kbd> to create headings. One <kbd>#</kbd> creates the largest heading (h1), two <kbd>##</kbd> creates a smaller heading (h2), and so on up to six levels.<br><br><pre># Main Title\n## Section\n### Subsection</pre>",
"task": "Create a main heading by typing <kbd># Hello</kbd>",
"previewHTML": "",
"previewBaseCSS": "",
"sandboxCSS": "",
"initialCode": "",
"solution": "# Hello",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "^#\\s+.+",
"message": "Start with <kbd>#</kbd> followed by a space and your heading text"
},
{
"type": "contains",
"value": "Hello",
"message": "Your heading should contain <kbd>Hello</kbd>"
}
]
},
{
"id": "md-heading-levels",
"title": "Heading Levels",
"description": "Use more <kbd>#</kbd> symbols for smaller headings. <kbd>##</kbd> creates an h2, <kbd>###</kbd> an h3. This creates a clear document structure with visual hierarchy.",
"task": "Create an h2 heading with <kbd>## About</kbd> followed by an h3 heading with <kbd>### Details</kbd>",
"previewHTML": "",
"previewBaseCSS": "",
"sandboxCSS": "",
"initialCode": "",
"solution": "## About\n\n### Details",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "^##\\s+About",
"message": "Start with <kbd>## About</kbd>"
},
{
"type": "regex",
"value": "###\\s+Details",
"message": "Add <kbd>### Details</kbd> for the h3 heading"
}
]
},
{
"id": "md-bold",
"title": "Bold Text",
"description": "Wrap text in double asterisks <kbd>**</kbd> or double underscores <kbd>__</kbd> to make it <strong>bold</strong>. This emphasizes important words or phrases.",
"task": "Make the word <kbd>important</kbd> bold by wrapping it with <kbd>**</kbd>",
"previewHTML": "",
"previewBaseCSS": "",
"sandboxCSS": "",
"initialCode": "This is important text.",
"solution": "This is **important** text.",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "\\*\\*important\\*\\*",
"message": "Wrap <kbd>important</kbd> with double asterisks: <kbd>**important**</kbd>"
}
]
},
{
"id": "md-italic",
"title": "Italic Text",
"description": "Wrap text in single asterisks <kbd>*</kbd> or single underscores <kbd>_</kbd> to make it <em>italic</em>. Use this for subtle emphasis or titles of works.",
"task": "Make the word <kbd>elegant</kbd> italic by wrapping it with <kbd>*</kbd>",
"previewHTML": "",
"previewBaseCSS": "",
"sandboxCSS": "",
"initialCode": "A simple and elegant solution.",
"solution": "A simple and *elegant* solution.",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "\\*elegant\\*",
"message": "Wrap <kbd>elegant</kbd> with single asterisks: <kbd>*elegant*</kbd>"
},
{
"type": "not_contains",
"value": "**elegant**",
"message": "Use single asterisks for italic, not double"
}
]
},
{
"id": "md-unordered-list",
"title": "Bullet Lists",
"description": "Create bullet lists using <kbd>-</kbd>, <kbd>*</kbd>, or <kbd>+</kbd> at the start of each line. Each item goes on its own line.",
"task": "Create a bullet list with three items: <kbd>Apple</kbd>, <kbd>Banana</kbd>, <kbd>Cherry</kbd>",
"previewHTML": "",
"previewBaseCSS": "",
"sandboxCSS": "",
"initialCode": "",
"solution": "- Apple\n- Banana\n- Cherry",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "^[-*+]\\s+Apple",
"message": "Start with a dash, asterisk, or plus followed by <kbd>Apple</kbd>"
},
{
"type": "regex",
"value": "[-*+]\\s+Banana",
"message": "Add <kbd>Banana</kbd> as a list item"
},
{
"type": "regex",
"value": "[-*+]\\s+Cherry",
"message": "Add <kbd>Cherry</kbd> as a list item"
}
]
},
{
"id": "md-ordered-list",
"title": "Numbered Lists",
"description": "Create numbered lists by starting lines with <kbd>1.</kbd>, <kbd>2.</kbd>, etc. Markdown automatically numbers them in sequence.",
"task": "Create a numbered list: <kbd>Wake up</kbd>, <kbd>Eat breakfast</kbd>, <kbd>Start coding</kbd>",
"previewHTML": "",
"previewBaseCSS": "",
"sandboxCSS": "",
"initialCode": "",
"solution": "1. Wake up\n2. Eat breakfast\n3. Start coding",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "\\d+\\.\\s+Wake up",
"message": "Start with a number and period: <kbd>1. Wake up</kbd>"
},
{
"type": "regex",
"value": "\\d+\\.\\s+Eat breakfast",
"message": "Add <kbd>Eat breakfast</kbd> as a numbered item"
},
{
"type": "regex",
"value": "\\d+\\.\\s+Start coding",
"message": "Add <kbd>Start coding</kbd> as a numbered item"
}
]
},
{
"id": "md-links",
"title": "Links",
"description": "Create links with <kbd>[text](url)</kbd>. The text in brackets is what readers see; the URL in parentheses is where they go when clicked.",
"task": "Create a link that shows <kbd>Google</kbd> and goes to <kbd>https://google.com</kbd>",
"previewHTML": "",
"previewBaseCSS": "",
"sandboxCSS": "",
"initialCode": "",
"solution": "[Google](https://google.com)",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "\\[Google\\]\\(https?://google\\.com\\)",
"message": "Use the format <kbd>[Google](https://google.com)</kbd>"
}
]
},
{
"id": "md-inline-code",
"title": "Inline Code",
"description": "Wrap text in backticks <kbd>`</kbd> to format it as code. This is useful for variable names, commands, or short code snippets in your text.",
"task": "Format <kbd>npm install</kbd> as inline code using backticks",
"previewHTML": "",
"previewBaseCSS": "",
"sandboxCSS": "",
"initialCode": "Run npm install to install dependencies.",
"solution": "Run `npm install` to install dependencies.",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "`npm install`",
"message": "Wrap <kbd>npm install</kbd> with backticks: <kbd>`npm install`</kbd>"
}
]
}
]
}

View File

@@ -1,139 +0,0 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "js-variables",
"title": "JS Variables",
"description": "Learn to declare variables with let and const, and work with basic data types in JavaScript.",
"mode": "javascript",
"difficulty": "beginner",
"lessons": [
{
"id": "js-const",
"title": "Constants",
"description": "Use <kbd>const</kbd> to declare a variable that cannot be reassigned. Constants are the default choice for most values in modern JavaScript.",
"task": "Declare a constant named <kbd>name</kbd> with the value <kbd>\"Alice\"</kbd>",
"previewHTML": "<p id=\"out\">Waiting...</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "",
"codeSuffix": "\ndocument.getElementById('out').textContent = name;",
"solution": "const name = \"Alice\";",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "const",
"message": "Use <kbd>const</kbd> to declare the variable"
},
{
"type": "regex",
"value": "const\\s+name\\s*=",
"message": "Declare a constant called <kbd>name</kbd>"
},
{
"type": "regex",
"value": "\"Alice\"|'Alice'|`Alice`",
"message": "Set the value to <kbd>\"Alice\"</kbd>"
}
]
},
{
"id": "js-let",
"title": "Let Variables",
"description": "Use <kbd>let</kbd> to declare variables that you plan to reassign later. Unlike <kbd>const</kbd>, a <kbd>let</kbd> variable can change its value.",
"task": "Declare a variable <kbd>count</kbd> with <kbd>let</kbd> set to <kbd>0</kbd>, then reassign it to <kbd>5</kbd>",
"previewHTML": "<p id=\"out\">Waiting...</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "",
"codeSuffix": "\ndocument.getElementById('out').textContent = count;",
"solution": "let count = 0;\ncount = 5;",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "let\\s+count\\s*=\\s*0",
"message": "Start with <kbd>let count = 0;</kbd>"
},
{
"type": "regex",
"value": "count\\s*=\\s*5",
"message": "Reassign count to <kbd>5</kbd>"
}
]
},
{
"id": "js-string",
"title": "Template Literals",
"description": "Template literals use backticks <kbd>`</kbd> and <kbd>${}</kbd> to embed expressions inside strings. This makes building dynamic text much easier than string concatenation.",
"task": "Create a constant <kbd>msg</kbd> using a template literal: <kbd>`Hello, ${name}!`</kbd>",
"previewHTML": "<p id=\"out\">Waiting...</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "const name = \"World\";\n",
"codeSuffix": "\ndocument.getElementById('out').textContent = msg;",
"solution": "const msg = `Hello, ${name}!`;",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "const\\s+msg\\s*=",
"message": "Declare a constant called <kbd>msg</kbd>"
},
{
"type": "contains",
"value": "${name}",
"message": "Use <kbd>${name}</kbd> inside backticks to embed the variable"
},
{
"type": "regex",
"value": "`.*\\$\\{name\\}.*`",
"message": "Wrap the whole string in backticks <kbd>`</kbd>"
}
]
},
{
"id": "js-array",
"title": "Arrays",
"description": "Arrays store ordered lists of values in square brackets. Access items by index (starting at 0) and use <kbd>.length</kbd> to get the count.",
"task": "Create a constant <kbd>colors</kbd> with an array: <kbd>[\"red\", \"green\", \"blue\"]</kbd>",
"previewHTML": "<p id=\"out\">Waiting...</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "",
"codeSuffix": "\ndocument.getElementById('out').textContent = colors.join(', ');",
"solution": "const colors = [\"red\", \"green\", \"blue\"];",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "const\\s+colors\\s*=",
"message": "Declare a constant called <kbd>colors</kbd>"
},
{
"type": "contains",
"value": "[",
"message": "Use square brackets <kbd>[</kbd> to create an array"
},
{
"type": "regex",
"value": "(\"red\"|'red'|`red`)",
"message": "Include <kbd>\"red\"</kbd> in the array"
},
{
"type": "regex",
"value": "(\"green\"|'green'|`green`)",
"message": "Include <kbd>\"green\"</kbd> in the array"
},
{
"type": "regex",
"value": "(\"blue\"|'blue'|`blue`)",
"message": "Include <kbd>\"blue\"</kbd> in the array"
}
]
}
]
}

View File

@@ -1,139 +0,0 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "js-dom",
"title": "JS DOM",
"description": "Learn to select and modify HTML elements using JavaScript DOM methods like querySelector and textContent.",
"mode": "javascript",
"difficulty": "beginner",
"lessons": [
{
"id": "js-query",
"title": "querySelector",
"description": "Use <kbd>document.querySelector()</kbd> to find the first element matching a CSS selector. It returns a single element you can then modify.",
"task": "Select the <kbd>h1</kbd> element and store it in a constant called <kbd>title</kbd>",
"previewHTML": "<h1>Hello</h1><p id=\"out\">Waiting...</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "",
"codeSuffix": "\ndocument.getElementById('out').textContent = title.tagName;",
"solution": "const title = document.querySelector('h1');",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "querySelector",
"message": "Use <kbd>document.querySelector()</kbd> to select an element"
},
{
"type": "regex",
"value": "querySelector\\(['\"`]h1['\"`]\\)",
"message": "Pass <kbd>'h1'</kbd> as the selector"
},
{
"type": "regex",
"value": "const\\s+title\\s*=",
"message": "Store the result in a constant called <kbd>title</kbd>"
}
]
},
{
"id": "js-text",
"title": "textContent",
"description": "The <kbd>textContent</kbd> property lets you read or change the text inside an element. Setting it replaces all existing text.",
"task": "Select the <kbd>.msg</kbd> element and set its <kbd>textContent</kbd> to <kbd>\"Done!\"</kbd>",
"previewHTML": "<p class=\"msg\">Waiting...</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "",
"codeSuffix": "",
"solution": "document.querySelector('.msg').textContent = \"Done!\";",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "querySelector",
"message": "Use <kbd>querySelector</kbd> to find the element"
},
{
"type": "contains",
"value": "textContent",
"message": "Use the <kbd>textContent</kbd> property to change the text"
},
{
"type": "regex",
"value": "(\"Done!\"|'Done!'|`Done!`)",
"message": "Set the text to <kbd>\"Done!\"</kbd>"
}
]
},
{
"id": "js-style",
"title": "Inline Styles",
"description": "Access the <kbd>style</kbd> property to set inline CSS on an element. CSS properties with dashes become camelCase: <kbd>background-color</kbd> becomes <kbd>backgroundColor</kbd>.",
"task": "Select the <kbd>.box</kbd> element and set its <kbd>style.color</kbd> to <kbd>\"coral\"</kbd>",
"previewHTML": "<p class=\"box\">Style me!</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { font-size: 1.5rem; font-weight: bold; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "",
"codeSuffix": "",
"solution": "document.querySelector('.box').style.color = \"coral\";",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "querySelector",
"message": "Use <kbd>querySelector</kbd> to find the element"
},
{
"type": "contains",
"value": ".style.",
"message": "Use the <kbd>.style</kbd> property to set CSS"
},
{
"type": "regex",
"value": "style\\.color\\s*=",
"message": "Set <kbd>style.color</kbd> on the element"
},
{
"type": "regex",
"value": "(\"coral\"|'coral'|`coral`)",
"message": "Set the color to <kbd>\"coral\"</kbd>"
}
]
},
{
"id": "js-classlist",
"title": "classList",
"description": "The <kbd>classList</kbd> property provides methods to add, remove, or toggle CSS classes on an element without touching other classes.",
"task": "Select the <kbd>.card</kbd> element and add the class <kbd>\"active\"</kbd> using <kbd>classList.add()</kbd>",
"previewHTML": "<div class=\"card\">Toggle me</div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .card { padding: 1rem; border: 2px solid gray; border-radius: 8px; } .active { border-color: coral; background: #fff0ee; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "",
"codeSuffix": "",
"solution": "document.querySelector('.card').classList.add(\"active\");",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "classList",
"message": "Use the <kbd>classList</kbd> property"
},
{
"type": "regex",
"value": "classList\\.add\\(",
"message": "Call <kbd>classList.add()</kbd> to add a class"
},
{
"type": "regex",
"value": "(\"active\"|'active'|`active`)",
"message": "Add the class <kbd>\"active\"</kbd>"
}
]
}
]
}

View File

@@ -1,118 +0,0 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "js-events",
"title": "JS Events",
"description": "Learn to respond to user interactions with addEventListener for clicks, input changes, and keyboard events.",
"mode": "javascript",
"difficulty": "beginner",
"lessons": [
{
"id": "js-click",
"title": "Click Events",
"description": "Use <kbd>addEventListener('click', ...)</kbd> to run code when a user clicks an element. The first argument is the event name, the second is a callback function.",
"task": "Add a click listener to the <kbd>.btn</kbd> element that sets the <kbd>.msg</kbd> text to <kbd>\"Clicked!\"</kbd>",
"previewHTML": "<button class=\"btn\">Click me</button><p class=\"msg\">Waiting...</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { padding: 0.5rem 1rem; border: none; background: steelblue; color: white; border-radius: 4px; cursor: pointer; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "const btn = document.querySelector('.btn');\nconst msg = document.querySelector('.msg');\n\n",
"codeSuffix": "",
"solution": "btn.addEventListener('click', () => {\n msg.textContent = \"Clicked!\";\n});",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "addEventListener",
"message": "Use <kbd>addEventListener</kbd> to listen for events"
},
{
"type": "regex",
"value": "addEventListener\\(['\"`]click['\"`]",
"message": "Listen for the <kbd>'click'</kbd> event"
},
{
"type": "contains",
"value": "textContent",
"message": "Use <kbd>textContent</kbd> to update the text"
},
{
"type": "regex",
"value": "(\"Clicked!\"|'Clicked!'|`Clicked!`)",
"message": "Set the text to <kbd>\"Clicked!\"</kbd>"
}
]
},
{
"id": "js-toggle",
"title": "Toggle Classes",
"description": "Combine events with <kbd>classList.toggle()</kbd> to switch a class on and off. Each click adds the class if missing, or removes it if present.",
"task": "Add a click listener to <kbd>.btn</kbd> that toggles the class <kbd>\"on\"</kbd> on <kbd>.lamp</kbd>",
"previewHTML": "<button class=\"btn\">Toggle</button><div class=\"lamp\">💡</div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; text-align: center; } .btn { padding: 0.5rem 1rem; border: none; background: steelblue; color: white; border-radius: 4px; cursor: pointer; } .lamp { font-size: 3rem; margin-top: 1rem; opacity: 0.3; transition: opacity 0.3s; } .lamp.on { opacity: 1; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "const btn = document.querySelector('.btn');\nconst lamp = document.querySelector('.lamp');\n\n",
"codeSuffix": "",
"solution": "btn.addEventListener('click', () => {\n lamp.classList.toggle('on');\n});",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "addEventListener",
"message": "Use <kbd>addEventListener</kbd> to listen for events"
},
{
"type": "regex",
"value": "addEventListener\\(['\"`]click['\"`]",
"message": "Listen for the <kbd>'click'</kbd> event"
},
{
"type": "regex",
"value": "classList\\.toggle\\(",
"message": "Use <kbd>classList.toggle()</kbd> to switch the class"
},
{
"type": "regex",
"value": "(\"on\"|'on'|`on`)",
"message": "Toggle the class <kbd>\"on\"</kbd>"
}
]
},
{
"id": "js-input",
"title": "Input Events",
"description": "The <kbd>input</kbd> event fires every time the value of an input field changes. Use <kbd>event.target.value</kbd> to read the current value.",
"task": "Add an input listener to <kbd>.field</kbd> that sets <kbd>.out</kbd> text to the input's value",
"previewHTML": "<input class=\"field\" placeholder=\"Type here...\"><p class=\"out\">Echo: </p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .field { padding: 0.5rem; border: 2px solid #ccc; border-radius: 4px; font-size: 1rem; width: 100%; box-sizing: border-box; }",
"sandboxCSS": "",
"initialCode": "",
"codePrefix": "const field = document.querySelector('.field');\nconst out = document.querySelector('.out');\n\n",
"codeSuffix": "",
"solution": "field.addEventListener('input', (event) => {\n out.textContent = event.target.value;\n});",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "addEventListener",
"message": "Use <kbd>addEventListener</kbd> to listen for events"
},
{
"type": "regex",
"value": "addEventListener\\(['\"`]input['\"`]",
"message": "Listen for the <kbd>'input'</kbd> event"
},
{
"type": "contains",
"value": "textContent",
"message": "Use <kbd>textContent</kbd> to update the output"
},
{
"type": "regex",
"value": "(event|e|evt)\\.target\\.value",
"message": "Read the input value with <kbd>event.target.value</kbd>"
}
]
}
]
}

View File

@@ -1,25 +0,0 @@
{
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "playground",
"title": "Playground",
"description": "Experiment freely with HTML and CSS",
"mode": "playground",
"difficulty": "beginner",
"excludeFromProgress": true,
"lessons": [
{
"id": "sandbox",
"title": "HTML & CSS Editor",
"mode": "playground",
"description": "",
"task": "",
"previewHTML": "",
"previewBaseCSS": "",
"sandboxCSS": "",
"initialCode": "<style>\n body {\n font-family: system-ui, sans-serif;\n padding: 20px;\n }\n \n h1 {\n color: steelblue;\n }\n</style>\n\n<h1>Hello World</h1>\n<p>Write any HTML and CSS here!</p>",
"solution": "",
"previewContainer": "preview-area",
"validations": []
}
]
}

View File

@@ -2,21 +2,20 @@
"$schema": "../schemas/code-crispies-module-schema.json",
"id": "goodbye",
"title": "What's Next?",
"description": "Continue your learning journey",
"description": "Congratulations on completing your learning journey!",
"mode": "html",
"difficulty": "beginner",
"excludeFromProgress": true,
"lessons": [
{
"id": "next-steps",
"title": "Keep Going!",
"description": "<strong>Great progress!</strong> You're building real web development skills.<br><br><strong>Continue learning:</strong><br>• <a href=\"https://developer.mozilla.org\" target=\"_blank\">MDN Web Docs</a> - The definitive reference<br>• <a href=\"https://css-tricks.com\" target=\"_blank\">CSS-Tricks</a> - Practical techniques<br><br><strong>Practice ideas:</strong><br>• Build your portfolio site<br>• Recreate a website you like<br>• Try the <a href=\"#playground/0\">Playground</a> to experiment freely<br><br><strong>Contribute:</strong> Code Crispies is <a href=\"https://git.librete.ch/libretech/code-crispies\" target=\"_blank\">open source</a>. Add lessons, fix bugs, or translate!<br><br><strong>Support:</strong> Help keep it free and open source!<br><a href=\"https://liberapay.com/libretech/donate\" target=\"_blank\" onclick=\"typeof umami !== 'undefined' && umami.track('support_click', {location: 'goodbye'})\"><img alt=\"Donate using Liberapay\" src=\"https://liberapay.com/assets/widgets/donate.svg\" style=\"margin-top: 8px;\"></a>",
"id": "congratulations",
"title": "Well Done!",
"description": "<strong>Congratulations!</strong> You've made it through Code Crispies!<br><br>You've learned:<br>• <strong>HTML</strong> - Semantic elements, forms, tables, and more<br>• <strong>CSS</strong> - Selectors, box model, flexbox, grid, animations<br>• <strong>Responsive Design</strong> - Media queries and mobile-first layouts<br><br>These are the building blocks of every website on the internet. Keep practicing and building!",
"task": "Type <code>Thank you!</code>",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; font-size: 1.5rem; color: #6366f1; }",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; } p { font-size: 1.5rem; color: #6366f1; }",
"sandboxCSS": "",
"initialCode": "",
"solution": "Thank you!",
"solution": "<p>Thank you!</p>",
"previewContainer": "preview-area",
"validations": [
{
@@ -25,6 +24,44 @@
"message": "Type <code>Thank you!</code>"
}
]
},
{
"id": "contribute",
"title": "Contribute",
"description": "<strong>Help others learn!</strong><br><br>Code Crispies is open source and always looking for new lessons. You can contribute by:<br><br>• <strong>Writing new lessons</strong> - Share your knowledge on topics we haven't covered<br>• <strong>Translating lessons</strong> - Help make Code Crispies accessible in more languages<br>• <strong>Fixing bugs</strong> - Found something broken? Help us fix it!<br>• <strong>Improving content</strong> - Make existing lessons clearer and better<br><br><strong>Get started:</strong><br>• <a href=\"https://git.librete.ch/libretech/code-crispies\" target=\"_blank\">Gitea Repository</a> - Main source code<br>• <a href=\"https://github.com/nextlevelshit/code-crispies\" target=\"_blank\">GitHub Mirror</a> - Alternative access<br><br>Lessons are simple JSON files in the <code>lessons/</code> folder. Check existing lessons for the format!",
"task": "Click Next to continue",
"previewHTML": "<div class=\"card\"><h2>Your First Lesson</h2><p>Create a JSON file in <code>lessons/</code></p><pre>{\n \"id\": \"my-lesson\",\n \"title\": \"My Lesson\",\n \"lessons\": [...]\n}</pre></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; } .card { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 20px; } h2 { color: #6366f1; margin-top: 0; } pre { background: #1e293b; color: #e2e8f0; padding: 12px; border-radius: 4px; overflow-x: auto; }",
"sandboxCSS": "",
"initialCode": "<div class=\"card\">\n <h2>Your First Lesson</h2>\n <p>Create a JSON file in <code>lessons/</code></p>\n <pre>{\n \"id\": \"my-lesson\",\n \"title\": \"My Lesson\",\n \"lessons\": [...]\n}</pre>\n</div>",
"solution": "<div class=\"card\">\n <h2>Your First Lesson</h2>\n <p>Create a JSON file in <code>lessons/</code></p>\n <pre>{\n \"id\": \"my-lesson\",\n \"title\": \"My Lesson\",\n \"lessons\": [...]\n}</pre>\n</div>",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "card",
"message": "Click Next to continue"
}
]
},
{
"id": "keep-learning",
"title": "Keep Learning",
"description": "<strong>Your journey continues!</strong><br><br><strong>Recommended resources:</strong><br>• <a href=\"https://developer.mozilla.org/en-US/docs/Web\" target=\"_blank\">MDN Web Docs</a> - The definitive web development reference<br>• <a href=\"https://css-tricks.com\" target=\"_blank\">CSS-Tricks</a> - Practical CSS techniques and tutorials<br>• <a href=\"https://web.dev\" target=\"_blank\">web.dev</a> - Modern web development best practices<br><br><strong>Practice projects:</strong><br>• Build your personal portfolio website<br>• Recreate your favorite website's layout<br>• Create a responsive landing page<br>• Build a CSS-only interactive component<br><br><strong>Remember:</strong> The best way to learn is by building. Start small, iterate, and have fun!<br><br><em>Thank you for learning with Code Crispies!</em>",
"task": "Explore the Playground",
"previewHTML": "<div class=\"message\"><span class=\"emoji\">🎉</span><h1>Happy Coding!</h1><p>See you in the code!</p></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; display: flex; align-items: center; justify-content: center; min-height: 200px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .message { text-align: center; color: white; } .emoji { font-size: 4rem; display: block; margin-bottom: 1rem; } h1 { margin: 0 0 0.5rem; } p { margin: 0; opacity: 0.9; }",
"sandboxCSS": "",
"initialCode": "<div class=\"message\">\n <span class=\"emoji\">🎉</span>\n <h1>Happy Coding!</h1>\n <p>See you in the code!</p>\n</div>",
"solution": "<div class=\"message\">\n <span class=\"emoji\">🎉</span>\n <h1>Happy Coding!</h1>\n <p>See you in the code!</p>\n</div>",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "message",
"message": "Explore the Playground"
}
]
}
]
}

View File

@@ -1,257 +1,548 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "css-basic-selectors",
"title": "أساسيات CSS",
"description": "تعلم اللبنات الأساسية لـ CSS: الخصائص والقيم والمحددات. يعلمك هذا الوحدة قواعد الصياغة التي يتبعها كل إعلان CSS.",
"title": "CSS Selectors",
"description": "CSS selectors are the foundation of styling web pages, allowing you to target specific HTML elements for styling. This module introduces fundamental selector types including element type selectors, class selectors, ID selectors, and the universal selector.",
"difficulty": "beginner",
"lessons": [
{
"id": "css-properties",
"title": "خصائص CSS",
"description": "تقوم CSS بتنسيق العناصر باستخدام <strong>الإعلانات</strong> - أزواج من الخصائص والقيم. كل إعلان يتبع نفس النمط:<br><br><pre>property: value;</pre><br><strong>الخصاصية</strong> هي ما تريد تغييره (مثل <kbd>color</kbd> أو <kbd>background</kbd>). <strong>القيمة</strong> هي ما تضبطها عليه. النقطتان تفصلهما، والفاصلة المنقوطة تنهي السطر.<br><br>القيم تأتي بأنواع مختلفة:<br>• <strong>كلمات مفتاحية:</strong> <kbd>red</kbd>، <kbd>bold</kbd>، <kbd>center</kbd><br>• <strong>أرقام بوحدات:</strong> <kbd>16px</kbd>، <kbd>2rem</kbd>، <kbd>100%</kbd><br>• <strong>ألوان:</strong> <kbd>steelblue</kbd>، <kbd>#ff0000</kbd>",
"task": "أكمل الإعلان بإضافة <kbd>color: coral;</kbd> لتغيير لون النص.",
"previewHTML": "<p class=\"text\">This text should turn coral.</p>",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } .text { font-size: 1.25rem; }",
"sandboxCSS": "",
"codePrefix": ".text {\n ",
"id": "introduction-to-selectors",
"title": "What's a Selector?",
"description": "A CSS selector is the first part of a CSS rule that tells the browser which HTML elements should receive the styles defined in the declaration block. Selectors are essentially patterns that match against elements in your HTML document. Understanding selectors is fundamental because they determine which elements your CSS rules will affect. The element or elements targeted by a selector are referred to as the 'subject of the selector.' When writing a CSS rule, you first specify the selector, followed by curly braces that contain the style declarations.<br/>For example, to change the text color of elements, you can use the <kbd>color</kbd> property within your declaration block.<br><br><pre>/* Element selector */\np {\n color: orangered;\n /* │ └─── Indicates the value of the expression\n │ \n └─────────── Indicates the property of the expression */\n}</pre>",
"task": "Write a CSS rule using a type selector that targets all paragraph elements <kbd>p</kbd> in the document. Make the text blue by setting the <kbd>color</kbd> property to <kbd>blue</kbd>.",
"previewHTML": "<h1>Introduction to CSS Selectors</h1>\n<p>This paragraph should turn blue.</p>\n<div>This div element should remain unchanged.</div>\n<p>This second paragraph should also turn blue.</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h1, p, div { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
"codePrefix": "/* Write a type selector to target all paragraph elements */\n",
"initialCode": "",
"codeSuffix": "\n}",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "color: coral;",
"solution": "p { color: blue }",
"validations": [
{
"type": "property_value",
"value": { "property": "color", "expected": "coral" },
"message": "أضف <kbd>color: coral;</kbd>"
}
]
},
{
"id": "multiple-properties",
"title": "خصائص متعددة",
"description": "يمكن أن تحتوي القاعدة على إعلانات متعددة. كل واحدة تذهب في سطرها الخاص، وكل واحدة تحتاج فاصلة منقوطة في النهاية:<br><br><pre>.box {<br> background: gold;<br> color: navy;<br> padding: 1rem;<br>}</pre><br>الترتيب عادةً لا يهم - CSS تطبقها جميعًا. عند التعارض، الأخيرة تفوز.",
"task": "أضف إعلانين: <kbd>background: lavender;</kbd> و <kbd>padding: 1rem;</kbd>",
"previewHTML": "<div class=\"card\">A styled card with background and padding.</div>",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } .card { border-radius: 8px; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"previewContainer": "preview-area",
"solution": "background: lavender;\n padding: 1rem;",
"validations": [
"type": "regex",
"value": "^p\\s*{",
"message": "Start your rule with <kbd>p { … }</kbd> to select all paragraph elements",
"options": {
"caseSensitive": false
}
},
{
"type": "property_value",
"value": { "property": "background", "expected": "lavender" },
"message": "أضف <kbd>background: lavender;</kbd>"
"type": "contains",
"value": "color:",
"message": "Include the <kbd>color:</kbd> property in your CSS rule"
},
{
"type": "contains",
"value": "blue",
"message": "Set the color value to <kbd>blue</kbd>"
},
{
"type": "property_value",
"value": { "property": "padding", "expected": "1rem" },
"message": "أضف <kbd>padding: 1rem;</kbd>"
"value": {
"property": "color",
"expected": "blue"
},
"message": "Use <kbd>color: blue</kbd> to set the text color"
},
{
"type": "regex",
"value": "p\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": false
}
}
]
},
{
"id": "type-selectors",
"title": "محددات النوع",
"description": "<strong>المحدد</strong> يخبر المتصفح أي العناصر يجب تنسيقها. أبسط محدد هو <strong>محدد النوع</strong> — مجرد اسم وسم HTML.<br><br><pre>p {<br> color: steelblue;<br>}</pre><br>هذه القاعدة تستهدف كل عنصر <kbd>&lt;p&gt;</kbd> في الصفحة. محددات النوع رائعة لتعيين الأنماط الأساسية.",
"task": "نسق جميع الفقرات. اكتب قاعدة مع <kbd>p</kbd> كمحدد واضبط <kbd>color: steelblue</kbd>.",
"previewHTML": "<article>\n <h2>Fresh Roasted Coffee</h2>\n <p>Our beans are sourced from small farms in Colombia and Ethiopia.</p>\n <p>Each batch is roasted weekly to ensure peak freshness.</p>\n</article>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } h2 { margin: 0 0 1rem; }",
"sandboxCSS": "",
"codePrefix": "",
"initialCode": "",
"title": "Type Selectors",
"description": "Type selectors (also called tag name selectors or element selectors) target HTML elements based on their tag name. For example, <kbd>p</kbd> selects all paragraph elements, <kbd>h1</kbd> selects all level-one headings, and <kbd>div</kbd> selects all division elements. Type selectors are the most fundamental way to select elements, applying styles consistently to all instances of a particular HTML element throughout your document. You can define a variety of CSS properties with type selectors, such as <kbd>color</kbd> for text color, <kbd>background-color</kbd> for the background, and <kbd>font-weight</kbd> for text emphasis. They provide a broad approach for styling your page and are often the starting point for more specific styling using other selector types.",
"task": "Write three separate CSS rules using type selectors to target specific HTML elements: make <kbd>h2</kbd> headings <kbd>purple</kbd>, give <kbd>span</kbd> elements a <kbd>yellow</kbd> background, and make <kbd>strong</kbd> elements <kbd>red</kbd>.",
"previewHTML": "<h2>Type Selectors Example</h2>\n<p>Regular paragraph text <span>with a highlighted span</span> that should have a yellow background.</p>\n<p>Another paragraph with <strong>strong important text</strong> that should be red.</p>\n<h2>Another Heading</h2>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h2, p, span, strong { padding: 3px; }",
"codePrefix": "/* Write three separate type selectors below */\n\n",
"initialCode": "/* 1. Make h2 headings purple */\n\n\n/* 2. Give span elements a yellow background */\n\n\n/* 3. Make strong elements red */\n",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "p {\n color: steelblue;\n}",
"solution": "/* 1. Make h2 headings purple */\nh2 {\n color: purple;\n}\n\n/* 2. Give span elements a yellow background */\nspan {\n background-color: yellow;\n}\n\n/* 3. Make strong elements red */\nstrong {\n color: red;\n}",
"validations": [
{
"type": "regex",
"value": "p\\s*\\{",
"message": "ابدأ بـ <kbd>p {</kbd> لاختيار الفقرات"
"value": "^h2\\s*{",
"message": "Include an <kbd>h2 { … }</kbd> selector"
},
{
"type": "property_value",
"value": { "property": "color", "expected": "steelblue" },
"message": "اضبط <kbd>color: steelblue</kbd>"
}
]
},
{
"id": "styling-links",
"title": "تنسيق الروابط",
"description": "محددات النوع تعمل مع أي عنصر HTML. المحدد <kbd>a</kbd> يستهدف جميع الروابط في الصفحة.<br><br>الروابط لها لون أزرق وخط تحتها افتراضيًا. يمكنك تغيير كليهما مع CSS — استخدم <kbd>color</kbd> للنص و <kbd>text-decoration: none</kbd> لإزالة الخط.",
"task": "نسق روابط التنقل. اكتب قاعدة مع <kbd>a</kbd> كمحدد واضبط <kbd>color: coral</kbd>.",
"previewHTML": "<nav>\n <a href=\"#\">Home</a>\n <a href=\"#\">Menu</a>\n <a href=\"#\">About</a>\n <a href=\"#\">Contact</a>\n</nav>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } nav { display: flex; gap: 1.5rem; }",
"sandboxCSS": "",
"codePrefix": "",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "a {\n color: coral;\n}",
"validations": [
"value": {
"property": "color",
"expected": "purple"
},
"message": "Set the <kbd>color</kbd> property to <kbd>purple</kbd> for h2 elements"
},
{
"type": "regex",
"value": "a\\s*\\{",
"message": "ابدأ بـ <kbd>a {</kbd> لاختيار الروابط"
"value": "h2\\s*{[^}]*}",
"message": "Make sure to close your h2 rule with a closing brace <kbd>}</kbd>"
},
{
"type": "regex",
"value": "^span\\s*{",
"message": "Include a <kbd>span { … }</kbd> selector"
},
{
"type": "property_value",
"value": { "property": "color", "expected": "coral" },
"message": "اضبط <kbd>color: coral</kbd>"
"value": {
"property": "background-color",
"expected": "yellow"
},
"message": "Set a <kbd>background-color: yellow</kbd> for span elements"
},
{
"type": "regex",
"value": "span\\s*{[^}]*}",
"message": "Make sure to close your span rule with a closing brace <kbd>}</kbd>"
},
{
"type": "regex",
"value": "^strong\\s*{",
"message": "Include a <kbd>strong { … }</kbd> selector"
},
{
"type": "regex",
"value": "strong\\s*{\\s*color:\\s*red;[^}]*}",
"message": "Set the <kbd>color: red</kbd> for strong elements"
}
]
},
{
"id": "class-selectors",
"title": "محددات الفئة",
"description": "محددات النوع تنسق <em>جميع</em> العناصر من ذلك النوع. لكن ماذا لو أردت تنسيق بعضها فقط؟<br><br><strong>محددات الفئة</strong> تستهدف العناصر ذات سمة <kbd>class</kbd> محددة. تبدأ بنقطة:<br><br><pre>.badge {<br> background: coral;<br>}</pre><br>هذا ينسق فقط العناصر ذات <kbd>class=\"badge\"</kbd>.",
"task": "نسق شارة الإشعارات. اكتب قاعدة مع <kbd>.badge</kbd> كمحدد واضبط <kbd>background: tomato</kbd>.",
"previewHTML": "<header>\n <h1>Dashboard</h1>\n <span class=\"badge\">3</span>\n</header>\n<p>You have new notifications waiting.</p>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem; } h1 { margin: 0; font-size: 1.5rem; } .badge { color: white; padding: 0.25rem 0.5rem; border-radius: 999px; font-size: 0.875rem; } p { color: #555; margin: 0; }",
"sandboxCSS": "",
"codePrefix": "",
"title": "Class Selectors",
"description": "Class selectors target elements with a specific class attribute value. They begin with a dot (.) followed by the class name. Classes are powerful because they allow you to apply the same styles to multiple elements regardless of their type. An HTML element can have multiple classes (separated by spaces in the class attribute), and a class can be applied to any number of elements. When using class selectors, you can apply properties like <kbd>background-color</kbd> to set the background color of elements, and <kbd>font-weight</kbd> to control text thickness, making text bold or normal. This flexibility makes class selectors one of the most commonly used methods for applying styles in CSS, allowing for modular and reusable styling across your website.",
"task": "Create a CSS rule using a class selector that targets elements with the class <kbd>highlight</kbd>. Give these elements a <kbd>yellow</kbd> background and <kbd>bold</kbd> text.",
"previewHTML": "<h2>Using Class Selectors</h2>\n<p>This is a regular paragraph, but <span class=\"highlight\">this span has the highlight class</span> applied to it.</p>\n<p class=\"highlight\">This entire paragraph has the highlight class.</p>\n<ul>\n <li>Regular list item</li>\n <li class=\"highlight\">This list item is highlighted</li>\n</ul>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h2, p, li { padding: 5px; margin-bottom: 10px; }",
"codePrefix": "/* Create a class selector for elements with the 'highlight' class */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": ".badge {\n background: tomato;\n}",
"validations": [
{
"type": "regex",
"value": "\\.badge\\s*\\{",
"message": "ابدأ بـ <kbd>.badge {</kbd> (لا تنسَ النقطة!)"
"value": "^\\.highlight\\s*{",
"message": "Start your rule with <kbd>.highlight { … }</kbd> to create a class selector",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "background-color:",
"message": "Include the <kbd>background-color:</kbd> property"
},
{
"type": "property_value",
"value": { "property": "background", "expected": "tomato" },
"message": "اضبط <kbd>background: tomato</kbd>"
"value": {
"property": "background-color",
"expected": "yellow"
},
"message": "Set the background color to <kbd>yellow</kbd>"
},
{
"type": "contains",
"value": "font-weight:",
"message": "Include the <kbd>font-weight:</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "font-weight",
"expected": "bold"
},
"message": "Set the font-weight to <kbd>bold</kbd>"
},
{
"type": "regex",
"value": "\\.highlight\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "button-variants",
"title": "متغيرات الأزرار",
"description": "يمكن أن تحتوي العناصر على فئات متعددة. عندما تسلسل محددات الفئة بدون مسافات، تستهدف العناصر التي لديها <em>جميع</em> تلك الفئات:<br><br><pre>.btn.primary {<br> background: steelblue;<br>}</pre><br>هذا يستهدف العناصر ذات <kbd>class=\"btn primary\"</kbd>، وليس فقط <kbd>.btn</kbd> أو فقط <kbd>.primary</kbd>.",
"task": "نسق الزر الأساسي. اكتب قاعدة مع <kbd>.btn.primary</kbd> كمحدد واضبط <kbd>background: steelblue</kbd>.",
"previewHTML": "<div class=\"actions\">\n <button class=\"btn\">Cancel</button>\n <button class=\"btn primary\">Save Changes</button>\n</div>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } .actions { display: flex; gap: 0.75rem; } .btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 1rem; cursor: pointer; background: #e0e0e0; color: #333; }",
"id": "multiple-classes",
"title": "Multiple Classes",
"description": "HTML elements can have multiple classes applied simultaneously, allowing for composable and modular CSS designs. When an element has multiple classes, it will receive styles from all matching class selectors. This approach enables you to build a library of reusable CSS classes that can be combined in different ways. You can also target elements that have a specific combination of classes by chaining class selectors together without spaces (e.g., <kbd>.class1.class2</kbd>). When styling these elements, you might use properties like <kbd>border-color</kbd> to change the color of element borders, and <kbd>background-color</kbd> to set the background color of elements. This technique lets you create conditional styles that only apply when certain classes appear together.",
"task": "Complete the CSS rule that targets elements with both <kbd>card</kbd> and <kbd>featured</kbd> classes by chaining the selectors. Set the border-color to gold and the background-color to lemonchiffon to make featured cards stand out.",
"previewHTML": "<h2>Multiple Class Combinations</h2>\n<div class=\"card\">Regular Card</div>\n<div class=\"card featured\">Featured Card</div>\n<div class=\"featured\">Just Featured (not a card)</div>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .card { border: 2px solid gray; padding: 15px; margin-bottom: 10px; border-radius: 5px; }",
"sandboxCSS": "",
"codePrefix": "",
"codePrefix": "/* The .card class already has basic styling */\n/* Now target elements with BOTH classes: 'card' AND 'featured' */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": ".btn.primary {\n background: steelblue;\n}",
"solution": ".card.featured { border-color: gold; background-color: lemonchiffon }",
"validations": [
{
"type": "regex",
"value": "\\.btn\\.primary\\s*\\{",
"message": "استخدم <kbd>.btn.primary {</kbd> (بدون مسافة بين الفئات)"
"value": "^\\.card\\.featured\\s*{",
"message": "Chain the selectors as <kbd>.card.featured</kbd> (no space between them)",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "border-color:",
"message": "Include the <kbd>border-color</kbd> property"
},
{
"type": "property_value",
"value": { "property": "background", "expected": "steelblue" },
"message": "اضبط <kbd>background: steelblue</kbd>"
"value": {
"property": "border-color",
"expected": "gold"
},
"message": "Set the border color to <kbd>gold</kbd>"
},
{
"type": "regex",
"value": "\\.card\\.featured\\s*{[^}]*;",
"message": "Make sure to end your CSS rule with a semicolon <kbd>;</kbd>"
},
{
"type": "contains",
"value": "background-color:",
"message": "Include the <kbd>background-color</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "background-color",
"expected": "lemonchiffon"
},
"message": "Set the background color to <kbd>lemonchiffon</kbd>"
},
{
"type": "regex",
"value": "\\.card\\.featured\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "specific-elements",
"title": "استهداف عناصر محددة",
"description": "أحيانًا تريد أن تبدو الفئة مختلفة على عناصر مختلفة. اجمع محدد النوع مع محدد الفئة (بدون مسافة) لتكون أكثر تحديدًا:<br><br><pre>a.btn {<br> text-decoration: none;<br>}</pre><br>هذا ينسق فقط عناصر <kbd>&lt;a&gt;</kbd> ذات الفئة <kbd>btn</kbd>، وليس عناصر <kbd>&lt;button&gt;</kbd> ذات تلك الفئة.",
"task": "أزل الخط من أزرار الروابط. اكتب قاعدة مع <kbd>a.btn</kbd> كمحدد واضبط <kbd>text-decoration: none</kbd>.",
"previewHTML": "<div class=\"actions\">\n <button class=\"btn\">Regular Button</button>\n <a href=\"#\" class=\"btn\">Link Button</a>\n</div>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } .actions { display: flex; gap: 0.75rem; align-items: center; } .btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 1rem; cursor: pointer; background: steelblue; color: white; }",
"sandboxCSS": "",
"codePrefix": "",
"id": "class-with-type",
"title": "Combining Types",
"description": "You can combine type selectors with class selectors to target specific HTML elements that have a certain class. This creates a more specific selector that only matches when both conditions are true: the element is of the specified type AND it has the specified class. For example, <kbd>p.note</kbd> would select paragraph elements with the class <kbd>note</kbd>, but would not select divs or spans with that same class. You can style these combined selections using properties like <kbd>background-color</kbd> to set a colored background for your elements. This approach allows you to apply different styles to the same class when it appears on different element types.",
"task": "Create a CSS rule that specifically targets <kbd>&lt;span&gt;</kbd> elements with the class <kbd>highlight</kbd>. Make those elements have an orange background, while other elements with the highlight class remain untouched.",
"previewHTML": "<h2>Type and Class Combinations</h2>\n<p>This paragraph has a <span class=\"highlight\">highlighted span</span> that should have an orange background.</p>\n<p class=\"highlight\">This paragraph has the highlight class but should NOT have an orange background.</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .highlight { font-weight: bold; }",
"sandboxCSS": "h2, p, span { padding: 5px; }",
"codePrefix": "/* The .highlight class already sets font-weight to bold */\n/* Now target ONLY span elements with the highlight class */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "a.btn {\n text-decoration: none;\n}",
"validations": [
{
"type": "regex",
"value": "a\\.btn\\s*\\{",
"message": "استخدم <kbd>a.btn {</kbd> (نوع + فئة، بدون مسافة)"
"value": "^span\\.highlight\\s*{",
"message": "Use <kbd>span.highlight</kbd> selector (no space between element and class)",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "background-color:",
"message": "Include the <kbd>background-color</kbd> property"
},
{
"type": "property_value",
"value": { "property": "text-decoration", "expected": "none" },
"message": "اضبط <kbd>text-decoration: none</kbd>"
"value": {
"property": "background-color",
"expected": "orange"
},
"message": "Set the background color to <kbd>orange</kbd>"
},
{
"type": "regex",
"value": "span\\.highlight\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "grouping-selectors",
"title": "تجميع المحددات",
"description": "عندما تحتاج عناصر متعددة لنفس الأنماط، اذكرها مفصولة بفواصل. هذا يحافظ على CSS نظيفًا وقابلاً للصيانة.<br><br><pre>h1, h2, h3 {<br> color: steelblue;<br>}</pre><br>هذا يطبق نفس اللون على مستويات العناوين الثلاثة في قاعدة واحدة.",
"task": "نسق جميع العناوين بشكل متسق. أضف <kbd>color: steelblue</kbd> إلى المحدد المجمع <kbd>h1, h2, h3</kbd>.",
"previewHTML": "<article><h1>Main Title</h1><p>Introduction paragraph with some text.</p><h2>Section Heading</h2><p>More content here.</p><h3>Subsection</h3><p>Final paragraph.</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } article { max-width: 500px; } p { color: #555; line-height: 1.6; }",
"sandboxCSS": "",
"codePrefix": "h1, h2, h3 {\n ",
"id": "id-selectors",
"title": "ID Selectors",
"description": "ID selectors target elements with a specific id attribute. They begin with a hash/pound sign (#) followed by the ID name. Unlike classes, IDs must be unique within a document—each ID value should be used only once per page. ID selectors have higher specificity than class or element selectors, meaning they override those selectors when conflicts arise. When styling with ID selectors, you can use properties like <kbd>color</kbd> to define text color, and <kbd>text-decoration</kbd> to control the appearance of text, such as adding underlines to elements. Because of their uniqueness requirement, IDs are best used for one-of-a-kind elements like page headers, main navigation, or specific unique components that appear only once on a page.",
"task": "Create a CSS rule with an ID selector that targets the element with the ID <kbd>main-title</kbd>. Set its color to purple and add an underline with <kbd>text-decoration: underline</kbd>.",
"previewHTML": "<h1 id=\"main-title\">Main Page Title</h1>\n<p>Regular paragraph content.</p>\n<h2>Secondary Heading</h2>\n<p id=\"intro\">Introduction paragraph (different ID).</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h1, h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
"codePrefix": "/* Create an ID selector to target the element with id=\"main-title\" */\n",
"initialCode": "",
"codeSuffix": "\n}",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "color: steelblue;",
"validations": [
{
"type": "regex",
"value": "^#main-title\\s*{",
"message": "Start your rule with <kbd>#main-title</kbd> to create an ID selector",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "color:",
"message": "Include the <kbd>color</kbd> property"
},
{
"type": "property_value",
"value": { "property": "color", "expected": "steelblue" },
"message": "اضبط <kbd>color: steelblue</kbd>"
"value": {
"property": "color",
"expected": "purple"
},
"message": "Set the color to <kbd>purple</kbd>"
},
{
"type": "contains",
"value": "text-decoration:",
"message": "Include the <kbd>text-decoration</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "text-decoration",
"expected": "underline"
},
"message": "Set the text-decoration to <kbd>underline</kbd>"
},
{
"type": "regex",
"value": "#main-title\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "descendant-selectors",
"title": "محددات الأحفاد",
"description": "استهدف العناصر داخل عناصر أخرى باستخدام مسافة بين المحددات. هذا أحد أكثر الأنماط فائدة في CSS.<br><br><pre>.nav a {<br> color: white;<br>}</pre><br>هذا ينسق فقط الروابط داخل <kbd>.nav</kbd>، تاركًا الروابط الأخرى دون تغيير.",
"task": "نسق روابط التنقل بشكل مختلف. اكتب قاعدة مع <kbd>.nav a</kbd> كمحدد واضبط <kbd>color: white</kbd>.",
"previewHTML": "<nav class=\"nav\"><a href=\"#\">Home</a><a href=\"#\">About</a><a href=\"#\">Contact</a></nav><p>Read more in our <a href=\"#\">documentation</a>.</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; margin: 0; } .nav { background: steelblue; padding: 1rem; display: flex; gap: 1rem; border-radius: 8px; margin-bottom: 1rem; } .nav a { text-decoration: none; } p a { color: steelblue; }",
"sandboxCSS": "",
"codePrefix": "",
"id": "id-with-type",
"title": "Type + ID",
"description": "Similar to how you can combine type and class selectors, you can also combine type selectors with ID selectors. For example, <kbd>h1#title</kbd> targets an h1 element with the ID 'title'. When using this combined approach, you can apply CSS properties like <kbd>font-style</kbd> to control the slant of the text, making it italic or normal. While this selector combination is more specific than using just the ID selector, it's often unnecessary since IDs should already be unique in a document. However, this technique can be useful for improving code readability or when you want to emphasize that a particular ID should only appear on a specific element type.",
"task": "Create a CSS rule that combines a type selector with an ID selector to target specifically a paragraph element with the ID <kbd>special</kbd>. Set its font style to italic.",
"previewHTML": "<h2 id=\"special\">Heading with ID \"special\" (should NOT be affected)</h2>\n<p id=\"special\">Paragraph with ID \"special\" (should become italic)</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
"codePrefix": "/* Create a combined type+ID selector for a paragraph with id=\"special\" */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": ".nav a {\n color: white;\n}",
"validations": [
{
"type": "regex",
"value": "\\.nav\\s+a\\s*\\{",
"message": "استخدم <kbd>.nav a {</kbd> (مسافة بين .nav و a)"
"value": "^p#special\\s*{",
"message": "Use <kbd>p#special</kbd> to target paragraphs with ID <kbd>special</kbd>",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "font-style:",
"message": "Include the <kbd>font-style</kbd> property"
},
{
"type": "property_value",
"value": { "property": "color", "expected": "white" },
"message": "اضبط <kbd>color: white</kbd>"
"value": {
"property": "font-style",
"expected": "italic"
},
"message": "Set the font-style to <kbd>italic</kbd>"
},
{
"type": "regex",
"value": "p#special\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "nested-styling",
"title": "الأنماط المتداخلة",
"description": "محددات الأحفاد تتيح لك إنشاء أنماط سياقية. نفس العنصر يمكن أن يبدو مختلفًا حسب مكان ظهوره.<br><br>على سبيل المثال، الفقرات في <kbd>.card</kbd> قد تكون أصغر من الفقرات في <kbd>article</kbd>.",
"task": "اجعل الفقرات داخل البطاقة أصغر. اكتب قاعدة مع <kbd>.card p</kbd> كمحدد واضبط <kbd>font-size: 0.9rem</kbd>.",
"previewHTML": "<article><h2>Article Title</h2><p>This is a regular article paragraph with normal-sized text for comfortable reading.</p><div class=\"card\"><strong>Quick Tip</strong><p>Card paragraphs should be slightly smaller to fit the compact design.</p></div><p>Back to regular article text here.</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } article { max-width: 500px; } h2 { color: steelblue; margin-top: 0; } p { line-height: 1.6; color: #444; } .card { background: #f0f4f8; padding: 1rem; border-radius: 8px; border-left: 4px solid steelblue; } .card strong { color: steelblue; display: block; margin-bottom: 0.5rem; }",
"sandboxCSS": "",
"codePrefix": "",
"id": "selector-lists",
"title": "Selector Lists",
"description": "When multiple elements need the same styling, you can group them together using a selector list (also known as grouping selectors). Selector lists are created by separating individual selectors with commas. This approach reduces repetition in your CSS, making it more maintainable and efficient. For example, <kbd>h1, h2, h3 { color: blue; }</kbd> applies the same blue color to all three heading levels. When styling multiple selectors at once, you can apply properties like <kbd>background-color</kbd> to set the background, <kbd>border-left</kbd> to create a left border with a specific thickness, style, and color, and <kbd>padding-left</kbd> to create space between the content and the left border. Whitespace around commas is optional, and each selector in the list can be any valid selector type-elements, classes, IDs, or even more complex selectors.",
"task": "Create a selector list that applies the same styles to three different elements: paragraphs with class <kbd>note</kbd>, list items with class <kbd>important</kbd>, and the element with ID <kbd>summary</kbd>. Give them a <kbd>lightyellow</kbd> background, a <kbd>gold</kbd> left border, and some left <kbd>padding</kbd>.",
"previewHTML": "<p class=\"note\">This is a note paragraph.</p>\n<ul>\n <li>Regular list item</li>\n <li class=\"important\">Important list item</li>\n</ul>\n<div id=\"summary\">Summary section</div>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "p, li, div { padding: 8px; margin-bottom: 8px; border: 1px dashed gray; }",
"codePrefix": "/* Create a selector list to apply the same styles to multiple different elements */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": ".card p {\n font-size: 0.9rem;\n}",
"solution": "p.note,\nli.important,\n#summary {\n background-color: lightyellow;\n border-left: 3px solid gold;\n padding-left: 10px\n}",
"validations": [
{
"type": "contains",
"value": "p.note",
"message": "Include <kbd>p.note</kbd> in your selector list",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "li.important",
"message": "Include <kbd>li.important</kbd> in your selector list",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "#summary",
"message": "Include <kbd>#summary</kbd> in your selector list",
"options": {
"caseSensitive": true
}
},
{
"type": "regex",
"value": "\\.card\\s+p\\s*\\{",
"message": "استخدم <kbd>.card p {</kbd> (مسافة بين .card و p)"
"value": "(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)",
"message": "Create a comma-separated list with all three selectors",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "background-color:",
"message": "Include the <kbd>background-color</kbd> property"
},
{
"type": "property_value",
"value": { "property": "font-size", "expected": "0.9rem" },
"message": "اضبط <kbd>font-size: 0.9rem</kbd>"
"value": {
"property": "background-color",
"expected": "lightyellow"
},
"message": "Set the background color to <kbd>lightyellow</kbd>"
},
{
"type": "contains",
"value": "border-left:",
"message": "Include the <kbd>border-left</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "border-left",
"expected": "3px solid gold"
},
"message": "Use <kbd>border-left: 3px solid gold</kbd> to create a left border"
},
{
"type": "contains",
"value": "padding-left:",
"message": "Include the <kbd>padding-left</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "padding-left",
"expected": "10px"
},
"message": "Use <kbd>padding-left: 10px</kbd> to add left padding"
}
]
},
{
"id": "universal-selector",
"title": "Universal (*)",
"description": "The universal selector is denoted by an asterisk (*) and matches any element of any type. It selects everything in the document or, when combined with other selectors, everything within a specific context. For example, <kbd>* { margin: 0; }</kbd> removes margins from all elements, while <kbd>article *</kbd> selects all elements inside article elements. When using the universal selector in combination with other selectors, you can apply properties like <kbd>margin</kbd> to control the spacing around elements. The universal selector is powerful but should be used carefully due to its broad impact. It's commonly used in CSS resets, to override default browser styling, or to target all children of a particular element.",
"task": "Use the universal selector to remove margins from all elements inside the container div. Create a rule using <kbd>div.container *</kbd> as the selector and set <kbd>margin: 0</kbd>.",
"previewHTML": "<div class=\"container\">\n <h2>Inside Container</h2>\n <p>This paragraph is inside the container.</p>\n <ul>\n <li>List item inside container</li>\n </ul>\n</div>\n<p>This paragraph is outside the container and should not be affected.</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } div.container { border: 2px solid navy; padding: 15px; background-color: lavender; } h2, p, ul, li { margin: 15px 0; }",
"sandboxCSS": "",
"codePrefix": "/* Use the universal selector to target all elements inside the container */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "^div\\.container\\s+\\*\\s*{",
"message": "Use <kbd>div.container *</kbd> selector (with a space between container and *)",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "margin:",
"message": "Include the <kbd>margin</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "margin",
"expected": "0"
},
"message": "Set margin to <kbd>0</kbd>"
},
{
"type": "regex",
"value": "div\\.container\\s+\\*\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "specificity-basics",
"title": "Specificity",
"description": "CSS specificity determines which styles take precedence when multiple conflicting rules target the same element. Specificity follows a hierarchical system: inline styles have the highest specificity, followed by ID selectors, then class/attribute/pseudo-class selectors, and finally element/pseudo-element selectors. This can be conceptualized as a four-part score (inline, ID, class, element). When creating multiple rules that may target the same elements, you can use the <kbd>color</kbd> property to set text colors, and specificity will determine which color is actually applied. Understanding specificity is crucial for predictable styling and debugging CSS conflicts. When two selectors have equal specificity, the one that comes last in the stylesheet wins.",
"task": "Examine the existing CSS rules and add a new rule with higher specificity to override the text color of the paragraph. Create a rule using '.content p' as the selector and set color: green.",
"previewHTML": "<div class=\"content\">\n <p>What color will this paragraph be? Look at the CSS rules and their specificity.</p>\n</div>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "p { border: 1px dashed gray; padding: 10px; }",
"codePrefix": "/* These CSS rules target the same paragraph but have different specificity */\n\n/* Rule 1: Element selector (lowest specificity) */\np {\n color: red;\n}\n\n/* Rule 2: Descendant selector (higher specificity than just 'p') */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "^\\.content\\s+p\\s*{",
"message": "Use <kbd>.content p</kbd> as your selector (note the space between)",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "color:",
"message": "Include the <kbd>color</kbd> property"
},
{
"type": "contains",
"value": "green",
"message": ""
}
]
}

View File

@@ -1,17 +1,16 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "welcome",
"title": "مرحباً",
"description": "ابدأ مع Code Crispies",
"mode": "css",
"title": "Code Crispies",
"description": "Welcome to Code Crispies - your interactive web development learning platform",
"mode": "html",
"difficulty": "beginner",
"excludeFromProgress": true,
"lessons": [
{
"id": "hello",
"title": "أهلاً!",
"description": "<strong>مرحباً بك في Code Crispies!</strong> تعلم CSS و HTML من خلال تمارين عملية.<br><br><strong>كيف يعمل:</strong><br>1. اقرأ المهمة على اليسار<br>2. اكتب الكود في المحرر<br>3. شاهد النتائج مباشرة في المعاينة<br><br><strong>نصائح:</strong> استخدم <kbd>Ctrl+Z</kbd> للتراجع. افتح القائمة (☰) لاستكشاف جميع الوحدات.",
"task": "اكتب <code>Hello World</code> للبدء",
"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>",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; font-size: 2rem; color: #6366f1; font-weight: bold; }",
"sandboxCSS": "",
@@ -22,9 +21,42 @@
{
"type": "contains",
"value": "Hello World",
"message": "اكتب <code>Hello World</code>"
"message": "Write <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/libretech/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",
"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": "",
"initialCode": "<p>Hello World! 🌍</p>\n<p>Hallo Welt!</p>\n<p>Bonjour le monde!</p>\n<p>¡Hola Mundo!</p>\n<p>Ciao Mondo!</p>\n<p>Olá Mundo!</p>\n<p>こんにちは世界!</p>\n<p>你好世界!</p>\n<p>안녕 세상!</p>\n<p>Привет мир!</p>\n<p dir=\"rtl\">שלום עולם!</p>\n<p dir=\"rtl\">مرحبا بالعالم!</p>",
"solution": "<p>Hello World! 🌍</p>\n<p>Hallo Welt!</p>\n<p>Bonjour le monde!</p>\n<p>¡Hola Mundo!</p>\n<p>Ciao Mondo!</p>\n<p>Olá Mundo!</p>\n<p>こんにちは世界!</p>\n<p>你好世界!</p>\n<p>안녕 세상!</p>\n<p>Привет мир!</p>\n<p dir=\"rtl\">שלום עולם!</p>\n<p dir=\"rtl\">مرحبا بالعالم!</p>",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "Hello World",
"message": "Click Next to continue"
}
]
},
{
"id": "playground",
"title": "Playground",
"mode": "playground",
"description": "",
"task": "",
"previewHTML": "",
"previewBaseCSS": "",
"sandboxCSS": "",
"initialCode": "<style>\n body {\n font-family: system-ui, sans-serif;\n padding: 20px;\n }\n</style>\n\n<h1>Hello World</h1>\n<p>Start coding!</p>",
"solution": "",
"previewContainer": "preview-area",
"validations": []
}
]
}

View File

@@ -2,18 +2,18 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "box-model",
"title": "CSS Box Model",
"description": "أتقن المبادئ الأساسية لإدارة المساحة في تصميم الويب من خلال نموذج الصندوق CSS. يستكشف هذا الوحدة كيف يتحد المحتوى والحشو والحدود والهوامش لإنشاء هياكل التخطيط.",
"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.",
"difficulty": "beginner",
"lessons": [
{
"id": "box-model-1",
"title": "Padding",
"description": "كل عنصر في CSS هو صندوق بأربع طبقات: المحتوى، الحشو (padding)، الحدود، والهامش. <strong>Padding</strong> يخلق مساحة تنفس بين محتواك وحافة الصندوق.<br><br>بدون padding، يضغط النص بشكل محرج على الحدود. Padding يجعل المحتوى قابلاً للقراءة ومتوازناً بصرياً.<br><br><pre>.card {\n padding: 1rem;\n}</pre>",
"task": "النص داخل بطاقة الملف الشخصي ملتصق بالحواف. امنحه بعض المساحة الداخلية للتنفس.",
"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; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"codePrefix": ".box {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "padding: 1rem;",
@@ -22,62 +22,62 @@
{
"type": "property_value",
"value": { "property": "padding", "expected": "1rem" },
"message": "أي خاصية تضيف مساحة بين المحتوى وحافة العنصر؟"
"message": "Set <kbd>padding: 1rem</kbd>"
}
]
},
{
"id": "box-model-2",
"title": "Borders",
"description": "الحدود تنشئ حدوداً مرئية حول العناصر. اختصار <kbd>border</kbd> يقبل ثلاث قيم: العرض، النمط، واللون.<br><br>الأنماط الشائعة: <kbd>solid</kbd>، <kbd>dashed</kbd>، <kbd>dotted</kbd>، <kbd>none</kbd>",
"task": "هذه البطاقة تحتاج خطاً ملوناً كلمسة على حافتها اليسرى.",
"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; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"codePrefix": ".box {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "border-left: 4px solid steelblue;",
"solution": "border: 2px solid darkslategray;",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "border-left:\\s*4px\\s+solid\\s+steelblue",
"message": "استخدم الاختصار الذي يحدد حداً على جانب واحد فقط",
"value": "border:\\s*2px\\s+solid\\s+darkslategray",
"message": "Set <kbd>border: 2px solid darkslategray</kbd>",
"options": { "caseSensitive": false }
}
]
},
{
"id": "box-model-3",
"title": "Margins",
"description": "الهوامش تنشئ مساحة <em>خارج</em> العنصر، تفصله عن جيرانه. بينما يدفع padding المحتوى للداخل، الهوامش تدفع العناصر الأخرى بعيداً.",
"task": "بطاقتا الملف الشخصي ملتصقتان ببعضهما. أضف مساحة أسفل كل بطاقة للفصل بينهما.",
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article><article class=\"card\"><h3>Alex Rivera</h3><p>UX Designer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"codePrefix": ".outer {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "margin-bottom: 1rem;",
"solution": "margin: 1rem;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "margin-bottom", "expected": "1rem" },
"message": "أي خاصية تدفع العناصر المجاورة بعيداً من الأسفل؟"
"value": { "property": "margin", "expected": "1rem" },
"message": "Set <kbd>margin: 1rem</kbd>"
}
]
},
{
"id": "box-model-4",
"title": "Box Sizing",
"description": "افتراضياً، <kbd>width</kbd> يحدد فقط عرض المحتوى. Padding والحدود تُضاف للمجموع. هذا يسبب مشاكل في التخطيط.<br><br><kbd>box-sizing: border-box</kbd> يشمل padding والحدود في العرض، مما يجعل التحجيم متوقعاً. معظم المطورين يطبقون هذا على جميع العناصر.",
"task": "كلا البطاقتين بنفس العرض، لكن اليسرى تتجاوز لأن الحشو والحدود تُضاف فوق العرض. أصلح البطاقة اليمنى لتشمل الحشو والحدود في حجمها.",
"previewHTML": "<div class=\"demo\"><article class=\"card\">Content-box</article><article class=\"card fix\">Border-box</article></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .demo { display: flex; gap: 1rem; } .card { width: 200px; padding: 1rem; border: 4px solid steelblue; background: white; border-radius: 8px; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".fix {\n ",
"codePrefix": ".sized {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "box-sizing: border-box;",
@@ -86,104 +86,93 @@
{
"type": "property_value",
"value": { "property": "box-sizing", "expected": "border-box" },
"message": "أي وضع تحجيم يشمل padding والحدود في عرض العنصر؟"
"message": "Set <kbd>box-sizing: border-box</kbd>"
}
]
},
{
"id": "box-model-5",
"title": "Padding Shorthand",
"description": "Padding يقبل 1-4 قيم:<br>• قيمة واحدة: جميع الجوانب<br>• قيمتان: عمودي | أفقي<br>• 4 قيم: أعلى | يمين | أسفل | يسار",
"task": "هذا الزر ضيق جداً. امنحه مساحة على الجوانب أكثر من الأعلى والأسفل، باستخدام اختصار القيمتين.",
"previewHTML": "<button class=\"btn\">Follow</button>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".btn {\n ",
"codePrefix": ".first {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "padding: 8px 1rem;",
"solution": "margin-bottom: 2rem;",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "padding:\\s*8px\\s+1rem",
"message": "استخدم اختصار القيمتين: العمودي أولاً، ثم الأفقي",
"options": { "caseSensitive": false }
"type": "property_value",
"value": { "property": "margin-bottom", "expected": "2rem" },
"message": "Set <kbd>margin-bottom: 2rem</kbd>"
}
]
},
{
"id": "box-model-6",
"title": "Margin Shorthand",
"description": "Margin يستخدم نفس نمط الاختصار مثل padding. نمط شائع هو توسيط عناصر الكتلة أفقياً باستخدام <kbd>margin: 0 auto</kbd>.",
"task": "هذه البطاقة ملتصقة باليسار. وسّطها أفقياً باستخدام اختصار الهوامش مع هوامش جانبية تلقائية.",
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { width: 250px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"codePrefix": ".spaced {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "margin: 0 auto;",
"solution": "margin: 1rem 2rem;",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "margin:\\s*0\\s+auto",
"message": "استخدم الاختصار الذي يحسب هوامش أفقية متساوية تلقائياً",
"value": "margin:\\s*1rem\\s+2rem",
"message": "Set <kbd>margin: 1rem 2rem</kbd>",
"options": { "caseSensitive": false }
}
]
},
{
"id": "box-model-7",
"title": "Border Radius",
"description": "على الرغم من أنه ليس جزءاً من نموذج الصندوق الكلاسيكي، <kbd>border-radius</kbd> يُدوّر زوايا صندوق حدود العنصر. استخدم <kbd>50%</kbd> على عنصر مربع لإنشاء دائرة.",
"task": "صورة الأفاتار المربعة يجب أن تظهر كدائرة مثالية.",
"previewHTML": "<article class=\"card\"><img class=\"avatar\" src=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='35' r='25' fill='%23666'/%3E%3Ccircle cx='50' cy='90' r='40' fill='%23666'/%3E%3C/svg%3E\" alt=\"Avatar\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; text-align: center; } .avatar { width: 80px; height: 80px; background: #ddd; margin-bottom: 8px; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".avatar {\n ",
"codePrefix": ".padded {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "border-radius: 50%;",
"solution": "padding: 2rem;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "border-radius", "expected": "50%" },
"message": "أي خاصية تدوّر الزوايا؟ فكر في النسبة المئوية التي تصنع دائرة"
"value": { "property": "padding", "expected": "2rem" },
"message": "Set <kbd>padding: 2rem</kbd>"
}
]
},
{
"id": "box-model-8",
"title": "Complete Card",
"description": "لنجمع كل شيء معاً. بطاقة الإشعار هذه تحتاج تنسيقاً لتبدو احترافية.",
"task": "هذا الإشعار يحتاج ثلاثة أشياء: مساحة داخلية حتى لا يكون النص مزدحماً، لمسة ملونة على الحافة اليسرى، وزوايا مستديرة قليلاً.",
"previewHTML": "<div class=\"alert\"><strong>New message</strong><p>You have 3 unread notifications</p></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { background: seashell; } .alert strong { color: coral; } .alert p { margin: 4px 0 0; color: #666; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".alert {\n ",
"codePrefix": ".line {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "padding: 1rem;\n border-left: 4px solid coral;\n border-radius: 4px;",
"solution": "border-bottom: 4px solid dodgerblue;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "padding", "expected": "1rem" },
"message": "أضف مساحة داخلية للإشعار"
},
{
"type": "regex",
"value": "border-left:\\s*4px\\s+solid\\s+coral",
"message": "أضف لمسة ملونة على الحافة اليسرى",
"value": "border-bottom:\\s*4px\\s+solid\\s+dodgerblue",
"message": "Set <kbd>border-bottom: 4px solid dodgerblue</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "border-radius", "expected": "4px" },
"message": "نعّم زوايا الإشعار"
}
]
}

View File

@@ -1,100 +1,115 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "units-variables",
"title": "وحدات CSS والمتغيرات",
"description": "افهم تنوع وحدات القياس في CSS وكيفية تعريف واستخدام الخصائص المخصصة لأنماط قابلة للصيانة.",
"title": "CSS Units & Variables",
"description": "Understand the variety of CSS measurement units and how to define and use custom properties for maintainable styles.",
"difficulty": "beginner",
"lessons": [
{
"id": "units-1",
"title": "Relative Units",
"description": "يقدم CSS نوعين من الوحدات: <em>مطلقة</em> (مثل <kbd>px</kbd>) و<em>نسبية</em> (مثل <kbd>%</kbd> و <kbd>rem</kbd>). الوحدات النسبية تتكيف مع سياقها، مما يجعل التخطيطات مرنة وسهلة الوصول.<br><br><strong>الوحدات النسبية الشائعة:</strong><br>• <kbd>%</kbd> نسبة للعنصر الأب<br>• <kbd>rem</kbd> نسبة لحجم خط الجذر (عادة 16px)<br>• <kbd>em</kbd> نسبة لحجم خط العنصر<br><br>نمط شائع للمحتوى القابل للقراءة: اضبط <kbd>width: 100%</kbd> لملء المساحة المتاحة، ثم <kbd>max-width: 40rem</kbd> لتحديد طول السطر للقراءة.",
"task": "نص هذه المقالة عريض جداً على الشاشات الكبيرة. أضف <kbd>max-width: 40rem</kbd> للحصول على عرض قراءة مثالي.",
"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; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".article {\n ",
"codePrefix": "/* Set flexible sizing */\n.box {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "max-width: 40rem;",
"codeSuffix": "}",
"solution": " width: 80%;\n max-width: 37.5rem;",
"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": "40rem" },
"message": "اضبط <kbd>max-width: 40rem</kbd>"
"value": { "property": "max-width", "expected": "37.5rem" },
"message": "Set max-width to <kbd>37.5rem</kbd>"
}
]
},
{
"id": "units-2",
"title": "CSS Variables",
"description": "الخصائص المخصصة في CSS (المتغيرات) تتيح لك تعريف قيم قابلة لإعادة الاستخدام. عرّفها بـ <kbd>--اسم</kbd> واستخدمها بـ <kbd>var(--اسم)</kbd>. المتغيرات المعرّفة على <kbd>:root</kbd> متاحة في كل مكان.",
"task": "عرّف <kbd>--brand: steelblue</kbd> في <kbd>:root</kbd>، ثم استخدمها كلون <kbd>background</kbd> لـ <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; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ":root {\n ",
"codePrefix": "/* Define and use a CSS variable */\n:root {",
"initialCode": "",
"codeSuffix": "\n}\n\n.btn {\n background: var(--brand);\n}",
"solution": "--brand: steelblue;",
"codeSuffix": "}\n.themed { }",
"solution": " --main-color: #6200ee;\n}\n.themed {\n border-color: var(--main-color);",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "--brand",
"message": "عرّف المتغير <kbd>--brand</kbd>",
"value": "--main-color",
"message": "Define <kbd>--main-color</kbd> in :root",
"options": { "caseSensitive": false }
},
{
"type": "contains",
"value": "steelblue",
"message": "اضبط القيمة على <kbd>steelblue</kbd>",
"value": "var(--main-color)",
"message": "Use <kbd>var(--main-color)</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": "calc() Function",
"description": "دالة <kbd>calc()</kbd> تتيح لك خلط وحدات مختلفة في الحسابات. هذا ضروري للتخطيطات التي تجمع بين الأحجام الثابتة والمرنة، مثل تخطيط الشريط الجانبي.",
"task": "المحتوى الرئيسي يجب أن يملأ المساحة المتبقية بعد الشريط الجانبي 200px. اضبط <kbd>width: calc(100% - 200px)</kbd> على <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; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".main {\n ",
"codePrefix": "/* Use calc for dynamic sizing */\n.sized {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "width: calc(100% - 200px);",
"codeSuffix": "}",
"solution": " width: calc(100% - 2rem);\n min-height: calc(10vh + 1rem);",
"previewContainer": "preview-area",
"validations": [
{ "type": "contains", "value": "calc", "message": "Use <kbd>calc()</kbd> function", "options": { "caseSensitive": false } },
{
"type": "regex",
"value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
"message": "اضبط <kbd>width: calc(100% - 200px)</kbd>",
"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>",
"options": { "caseSensitive": false }
}
]
},
{
"id": "units-4",
"title": "Viewport Units",
"description": "وحدات العرض تحدد حجم العناصر نسبة لنافذة المتصفح:<br>• <kbd>vw</kbd> 1% من عرض العرض<br>• <kbd>vh</kbd> 1% من ارتفاع العرض<br><br>هذه مثالية للأقسام بملء الشاشة مثل لافتات hero.",
"task": "اجعل قسم hero هذا يملأ ارتفاع العرض بضبط <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; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".hero {\n ",
"codePrefix": "/* Use viewport units */\n.view {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "min-height: 100vh;",
"codeSuffix": "}",
"solution": " width: 50vw;\n height: 20vh;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "min-height", "expected": "100vh" },
"message": "اضبط <kbd>min-height: 100vh</kbd>"
}
{ "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>" }
]
}
]

View File

@@ -1,15 +1,15 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "transitions-animations",
"title": "حركات CSS",
"description": "أضف التفاعل لواجهتك من خلال انتقالات الخصائص السلسة والحركات المبنية على keyframes.",
"title": "CSS Animations",
"description": "Bring interactivity to your UI by smoothly transitioning properties and creating keyframe-driven animations.",
"difficulty": "intermediate",
"lessons": [
{
"id": "transitions-1",
"title": "Transitions",
"description": "تعلم كيفية تطبيق <kbd>transition</kbd> على الخصائص للتغييرات السلسة عند تغيير الحالة.<br><br><pre>transition: property duration;\n/* مثال: transition: background-color 0.3s; */</pre>",
"task": "أضف <kbd>transition: background-color 0.3s</kbd> ليتغير اللون بسلاسة عند التمرير.",
"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.",
"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": "استخدم خاصية <kbd>transition</kbd>",
"message": "Use the <kbd>transition</kbd> property",
"options": { "caseSensitive": false }
},
{
"type": "regex",
"value": "transition:\\s*background-color\\s*0\\.3s",
"message": "اضبط <kbd>transition: background-color 0.3s</kbd>",
"message": "Set <kbd>transition: background-color 0.3s</kbd>",
"options": { "caseSensitive": false }
}
]
@@ -36,8 +36,8 @@
{
"id": "transitions-2",
"title": "Timing Funcs",
"description": "استكشف دوال التسهيل مثل <kbd>ease</kbd>، <kbd>linear</kbd>، <kbd>ease-in</kbd>، <kbd>ease-out</kbd> للتحكم في إيقاع الحركة.",
"task": "اضبط <kbd>transition-timing-function</kbd> على <kbd>ease-in-out</kbd>.",
"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>.",
"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": "استخدم <kbd>transition-timing-function</kbd>",
"message": "Use <kbd>transition-timing-function</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
"message": "اضبط التوقيت على <kbd>ease-in-out</kbd>"
"message": "Set timing to <kbd>ease-in-out</kbd>"
}
]
},
{
"id": "transitions-3",
"title": "Keyframes",
"description": "أنشئ حركات مسماة باستخدام <kbd>@keyframes</kbd> وطبّقها عبر اختصار <kbd>animation</kbd>.<br><br><pre>@keyframes bounce {\n 50% { transform: translateY(-20px); }\n}\n.ball {\n animation: bounce 1s infinite;\n}</pre>",
"task": "عرّف keyframe عند <kbd>50%</kbd> مع <kbd>transform: translateY(-20px)</kbd> وطبّق <kbd>animation: bounce 1s infinite</kbd> على <kbd>.ball</kbd>.",
"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>.",
"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": "",
@@ -77,25 +77,25 @@
{
"type": "contains",
"value": "@keyframes bounce",
"message": "عرّف <kbd>@keyframes bounce</kbd>",
"message": "Define <kbd>@keyframes bounce</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "regex",
"value": "50%.*transform: translateY\\(-20px\\)",
"message": "عند <kbd>50%</kbd>، استخدم <kbd>transform: translateY(-20px)</kbd>",
"message": "At <kbd>50%</kbd>, use <kbd>transform: translateY(-20px)</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "contains",
"value": "animation",
"message": "استخدم خاصية <kbd>animation</kbd> على <kbd>.ball</kbd>",
"message": "Use <kbd>animation</kbd> property on <kbd>.ball</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "regex",
"value": "animation:.*bounce.*1s.*infinite",
"message": "طبّق <kbd>animation: bounce 1s infinite</kbd>",
"message": "Apply <kbd>animation: bounce 1s infinite</kbd>",
"options": { "caseSensitive": false }
}
]
@@ -103,8 +103,8 @@
{
"id": "transitions-4",
"title": "Animation Properties",
"description": "اضبط الحركات بـ <kbd>animation-delay</kbd>، <kbd>animation-iteration-count</kbd>، <kbd>animation-direction</kbd>، و <kbd>animation-fill-mode</kbd>.",
"task": "طبّق حركة <kbd>pulse</kbd> على <kbd>.box</kbd> مع <kbd>animation-name: pulse</kbd>، <kbd>animation-duration: 2s</kbd>، <kbd>animation-delay: 1s</kbd>، <kbd>animation-iteration-count: 2</kbd>، و <kbd>animation-fill-mode: forwards</kbd>.",
"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>.",
"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": "اضبط <kbd>animation-name: pulse</kbd>"
"message": "Set <kbd>animation-name: pulse</kbd>"
},
{
"type": "property_value",
"value": { "property": "animation-duration", "expected": "2s" },
"message": "اضبط <kbd>animation-duration: 2s</kbd>"
"message": "Set <kbd>animation-duration: 2s</kbd>"
},
{
"type": "property_value",
"value": { "property": "animation-delay", "expected": "1s" },
"message": "اضبط <kbd>animation-delay: 1s</kbd>"
"message": "Set <kbd>animation-delay: 1s</kbd>"
},
{
"type": "property_value",
"value": { "property": "animation-iteration-count", "expected": "2" },
"message": "اضبط <kbd>animation-iteration-count: 2</kbd>"
"message": "Set <kbd>animation-iteration-count: 2</kbd>"
},
{
"type": "property_value",
"value": { "property": "animation-fill-mode", "expected": "forwards" },
"message": "اضبط <kbd>animation-fill-mode: forwards</kbd>"
"message": "Set <kbd>animation-fill-mode: forwards</kbd>"
}
]
}

View File

@@ -2,14 +2,14 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "responsive-design",
"title": "CSS Responsive Design",
"description": "اجعل تخطيطاتك تتكيف مع أحجام الشاشات المختلفة باستخدام media queries وتقنيات التصميم المرن.",
"description": "Make your layouts adapt to different screen sizes using media queries and fluid design techniques.",
"difficulty": "intermediate",
"lessons": [
{
"id": "responsive-1",
"title": "Media Queries",
"description": "افهم صياغة واستخدامات CSS media queries لتطبيق الأنماط شرطياً بناءً على خصائص viewport.<br><br><pre>@media (max-width: 600px) {\n .panel {\n background: lightcoral;\n }\n}</pre>",
"task": "اكتب media query باستخدام <kbd>@media (max-width: 600px)</kbd> لتغيير خلفية <kbd>.panel</kbd> إلى <kbd>lightcoral</kbd>.",
"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>.",
"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": "استخدم <kbd>@media (max-width: 600px)</kbd>",
"message": "Use <kbd>@media (max-width: 600px)</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "contains",
"value": ".panel",
"message": "استهدف <kbd>.panel</kbd> داخل media query",
"message": "Target <kbd>.panel</kbd> inside the media query",
"options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "background", "expected": "lightcoral" },
"message": "اضبط <kbd>background: lightcoral</kbd>",
"message": "Set <kbd>background: lightcoral</kbd>",
"options": { "exact": false }
}
]
@@ -42,8 +42,8 @@
{
"id": "responsive-2",
"title": "Fluid Type",
"description": "استخدم وحدات نسبية مثل <kbd>vw</kbd> لجعل أحجام الخطوط تتناسب مع عرض viewport.",
"task": "اضبط <kbd>font-size: 5vw</kbd> لتتغير مع viewport.",
"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.",
"previewHTML": "<p class=\"text\">Fluid Typography</p>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }",
"sandboxCSS": "",
@@ -53,46 +53,46 @@
"solution": " font-size: 5vw;",
"previewContainer": "preview-area",
"validations": [
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "اضبط <kbd>font-size: 5vw</kbd>" }
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Set <kbd>font-size: 5vw</kbd>" }
]
},
{
"id": "responsive-3",
"title": "Responsive Grid",
"description": "ادمج CSS Grid مع <kbd>auto-fit</kbd> أو <kbd>auto-fill</kbd> لتخطيطات أعمدة متجاوبة تضبط عدد الأعمدة تلقائياً بناءً على المساحة المتاحة.",
"task": "أضف <kbd>display: grid</kbd> و <kbd>grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))</kbd> و <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; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".features {\n ",
"codePrefix": "/* Create a responsive grid */\n.cards {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;",
"codeSuffix": "}",
"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": "اضبط <kbd>display: grid</kbd>"
"message": "Set <kbd>display: grid</kbd>"
},
{
"type": "regex",
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
"message": "استخدم <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
"message": "Use <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "gap", "expected": "1rem" },
"message": "اضبط <kbd>gap: 1rem</kbd>"
"message": "Set <kbd>gap: 1rem</kbd>"
}
]
},
{
"id": "responsive-4",
"title": "Mobile-First",
"description": "اتبع نهج mobile-first بكتابة أنماط أساسية للشاشات الصغيرة وتحسينها لـ viewports أكبر.",
"task": "اكتب media query باستخدام <kbd>@media (min-width: 768px)</kbd> لضبط عرض <kbd>.sidebar</kbd> إلى <kbd>250px</kbd>.",
"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>.",
"previewHTML": "<aside class=\"sidebar\">Sidebar</aside>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }",
"sandboxCSS": "",
@@ -105,19 +105,19 @@
{
"type": "regex",
"value": "@media\\s*\\(min-width:\\s*768px\\)",
"message": "استخدم <kbd>@media (min-width: 768px)</kbd>",
"message": "Use <kbd>@media (min-width: 768px)</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "contains",
"value": ".sidebar",
"message": "استهدف <kbd>.sidebar</kbd> في media query",
"message": "Target <kbd>.sidebar</kbd> inside media query",
"options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "width", "expected": "250px" },
"message": "اضبط <kbd>width: 250px</kbd>",
"message": "Set <kbd>width: 250px</kbd>",
"options": { "exact": false }
}
]

View File

@@ -2,65 +2,94 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-elements",
"title": "HTML Block & Inline",
"description": "فهم الفرق الأساسي بين عناصر الحاويات (الكتلية) والعناصر السطرية",
"description": "Understanding the fundamental difference between container (block) and inline elements",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "block-vs-inline-intro",
"title": "العناصر الكتلية vs السطرية",
"description": "تنقسم عناصر HTML إلى فئتين رئيسيتين:<br><br><strong>العناصر الكتلية</strong> (الحاويات) تبدأ في سطر جديد وتأخذ العرض الكامل. أمثلة: <kbd>&lt;div&gt;</kbd>, <kbd>&lt;p&gt;</kbd>, <kbd>&lt;h1&gt;</kbd>, <kbd>&lt;section&gt;</kbd><br><br><strong>العناصر السطرية</strong> تتدفق داخل النص وتأخذ العرض المطلوب فقط. أمثلة: <kbd>&lt;span&gt;</kbd>, <kbd>&lt;a&gt;</kbd>, <kbd>&lt;strong&gt;</kbd>, <kbd>&lt;em&gt;</kbd>",
"task": "أحط الكلمة <kbd>مهمة</kbd> بوسوم <kbd>&lt;strong&gt;</kbd> لجعلها عريضة. لاحظ كيف يأخذ الفقرة (كتلي) العرض الكامل بينما strong (سطري) يتدفق مع النص.",
"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>&lt;div&gt;</kbd>, <kbd>&lt;p&gt;</kbd>, <kbd>&lt;h1&gt;</kbd>, <kbd>&lt;section&gt;</kbd><br><br><strong>Inline elements</strong> flow within text and only take needed width. Examples: <kbd>&lt;span&gt;</kbd>, <kbd>&lt;a&gt;</kbd>, <kbd>&lt;strong&gt;</kbd>, <kbd>&lt;em&gt;</kbd>",
"task": "Wrap the word <kbd>important</kbd> with <kbd>&lt;strong&gt;</kbd> tags to make it bold. Notice how the paragraph (block) takes full width while strong (inline) flows with text.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; } p { background: #e3f2fd; padding: 10px; } strong { background: #ffecb3; }",
"sandboxCSS": "",
"initialCode": "<p>هذه فقرة تحتوي على كلمة مهمة.</p>",
"solution": "<p>هذه فقرة تحتوي على كلمة <strong>مهمة</strong>.</p>",
"initialCode": "<p>This is a paragraph with an important word.</p>",
"solution": "<p>This is a paragraph with an <strong>important</strong> word.</p>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "p",
"message": "أضف عنصر فقرة <kbd>&lt;p&gt;</kbd>"
"message": "Add a <kbd>&lt;p&gt;</kbd> paragraph element"
},
{
"type": "parent_child",
"value": { "parent": "p", "child": "strong" },
"message": "أحط الكلمة <kbd>مهمة</kbd> بوسوم <kbd>&lt;strong&gt;</kbd>"
"message": "Wrap the word <kbd>important</kbd> with <kbd>&lt;strong&gt;</kbd> tags"
}
]
},
{
"id": "semantic-containers",
"title": "الوسوم الدلالية",
"description": "يستخدم HTML الحديث حاويات دلالية تصف محتواها:<br><br><kbd>&lt;header&gt;</kbd> - رأس الصفحة أو القسم<br><kbd>&lt;nav&gt;</kbd> - روابط التنقل<br><kbd>&lt;main&gt;</kbd> - منطقة المحتوى الرئيسي<br><kbd>&lt;section&gt;</kbd> - تجميع موضوعي<br><kbd>&lt;article&gt;</kbd> - محتوى مستقل<br><kbd>&lt;footer&gt;</kbd> - تذييل الصفحة أو القسم",
"task": "أنشئ هيكل صفحة أساسي:<br>1. أضف <kbd>&lt;header&gt;</kbd> مع <kbd>&lt;h1&gt;</kbd> يحتوي على النص <code>موقعي</code><br>2. أضف عنصر <kbd>&lt;main&gt;</kbd> مع فقرة تقول <code>مرحباً بك في موقعي!</code><br>3. أضف <kbd>&lt;footer&gt;</kbd> مع فقرة تقول <code>Copyright 2026</code>",
"title": "Semantic Tags",
"description": "Modern HTML uses semantic containers that describe their content:<br><br><kbd>&lt;header&gt;</kbd> - Page or section header<br><kbd>&lt;nav&gt;</kbd> - Navigation links<br><kbd>&lt;main&gt;</kbd> - Main content area<br><kbd>&lt;section&gt;</kbd> - Thematic grouping<br><kbd>&lt;article&gt;</kbd> - Self-contained content<br><kbd>&lt;footer&gt;</kbd> - Page or section footer",
"task": "Create a basic page structure:<br>1. Add a <kbd>&lt;header&gt;</kbd> with an <kbd>&lt;h1&gt;</kbd> containing the text <code>My Website</code><br>2. Add a <kbd>&lt;main&gt;</kbd> element with a paragraph saying <code>Welcome to my site!</code><br>3. Add a <kbd>&lt;footer&gt;</kbd> with a paragraph saying <code>Copyright 2026</code>",
"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>موقعي</h1>\n</header>\n<main>\n <p>مرحباً بك في موقعي!</p>\n</main>\n<footer>\n <p>Copyright 2026</p>\n</footer>",
"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>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "header",
"message": "أضف عنصر <kbd>&lt;header&gt;</kbd>"
"message": "Add a <kbd>&lt;header&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "main",
"message": "أضف عنصر <kbd>&lt;main&gt;</kbd>"
"message": "Add a <kbd>&lt;main&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "footer",
"message": "أضف عنصر <kbd>&lt;footer&gt;</kbd>"
"message": "Add a <kbd>&lt;footer&gt;</kbd> element"
},
{
"type": "parent_child",
"value": { "parent": "header", "child": "h1" },
"message": "أضف عنوان <kbd>&lt;h1&gt;</kbd> داخل header"
"message": "Add an <kbd>&lt;h1&gt;</kbd> heading inside your header"
}
]
},
{
"id": "div-vs-span",
"title": "div & span",
"description": "When you need a container without semantic meaning:<br><br><kbd>&lt;div&gt;</kbd> - Generic block container (for layout/grouping)<br><kbd>&lt;span&gt;</kbd> - Generic inline container (for styling text portions)<br><br>Use semantic elements when possible, div/span when no semantic element fits.",
"task": "Wrap the word 'highlighted' in a <kbd>&lt;span&gt;</kbd> to style it differently. Wrap the whole quote in a <kbd>&lt;div&gt;</kbd>.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: Georgia, serif; padding: 20px; } div { background: #f5f5f5; padding: 15px; border-left: 4px solid #1976d2; } span { background: #fff59d; padding: 2px 4px; }",
"sandboxCSS": "",
"initialCode": "The most highlighted moment was unforgettable.",
"solution": "<div>The most <span>highlighted</span> moment was unforgettable.</div>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "div",
"message": "Wrap everything in a <kbd>&lt;div&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "span",
"message": "Add a <kbd>&lt;span&gt;</kbd> around the word <kbd>highlighted</kbd>"
},
{
"type": "element_text",
"value": { "selector": "span", "text": "highlighted" },
"message": "The <kbd>&lt;span&gt;</kbd> should contain the word <kbd>highlighted</kbd>"
}
]
}

View File

@@ -1,100 +1,100 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-basic",
"title": "نماذج HTML",
"description": "تعلم إنشاء النماذج بأنواع حقول مختلفة",
"title": "HTML Forms",
"description": "Learn to create forms with various input types",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "form-structure",
"title": "هيكل النموذج",
"description": "كل نموذج يحتاج غلاف <kbd>&lt;form&gt;</kbd>. بداخله، استخدم <kbd>&lt;label&gt;</kbd> لوصف الحقول و <kbd>&lt;input&gt;</kbd> لإدخال البيانات.<br><br>سمة <kbd>for</kbd> في التسميات يجب أن تطابق <kbd>id</kbd> في الحقول للوصولية.",
"task": "أنشئ نموذجاً مع:<br>1. <kbd>&lt;label&gt;</kbd> بالنص <code>الاسم:</code> وسمة <kbd>for=\"name\"</kbd><br>2. <kbd>&lt;input&gt;</kbd> نصي بسمات <kbd>id=\"name\"</kbd> و <kbd>name=\"name\"</kbd>",
"title": "Form Structure",
"description": "Every form needs a <kbd>&lt;form&gt;</kbd> wrapper. Inside, use <kbd>&lt;label&gt;</kbd> to describe inputs and <kbd>&lt;input&gt;</kbd> for user data entry.<br><br>The <kbd>for</kbd> attribute on labels should match the <kbd>id</kbd> on inputs for accessibility.",
"task": "Create a form with:<br>1. A <kbd>&lt;label&gt;</kbd> with the text <code>Name:</code> and <kbd>for=\"name\"</kbd> attribute<br>2. A text <kbd>&lt;input&gt;</kbd> with <kbd>id=\"name\"</kbd> and <kbd>name=\"name\"</kbd> attributes",
"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\">الاسم:</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n</form>",
"solution": "<form>\n <label for=\"name\">Name:</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "form",
"message": "أحط كل شيء بعنصر <kbd>&lt;form&gt;</kbd>"
"message": "Wrap everything in a <kbd>&lt;form&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "label",
"message": "أضف <kbd>&lt;label&gt;</kbd> لحقلك"
"message": "Add a <kbd>&lt;label&gt;</kbd> for your input"
},
{
"type": "element_exists",
"value": "input",
"message": "أضف عنصر <kbd>&lt;input&gt;</kbd>"
"message": "Add an <kbd>&lt;input&gt;</kbd> element"
},
{
"type": "attribute_value",
"value": { "selector": "label", "attr": "for", "value": null },
"message": "أضف سمة <kbd>for</kbd> للتسمية"
"message": "Add a <kbd>for</kbd> attribute to your label"
},
{
"type": "attribute_value",
"value": { "selector": "input", "attr": "id", "value": null },
"message": "أضف سمة <kbd>id</kbd> لحقلك"
"message": "Add an <kbd>id</kbd> attribute to your input"
}
]
},
{
"id": "input-types",
"title": "أنواع الحقول",
"description": "أنواع الحقول المختلفة توفر لوحات مفاتيح وتحقق مناسب:<br><br><kbd>type=\"text\"</kbd> - نص عام<br><kbd>type=\"email\"</kbd> - بريد إلكتروني مع تحقق @<br><kbd>type=\"password\"</kbd> - أحرف مخفية<br><kbd>type=\"number\"</kbd> - لوحة مفاتيح رقمية<br><kbd>type=\"tel\"</kbd> - لوحة مفاتيح هاتف",
"task": "أنشئ نموذج تسجيل دخول بحقلين:<br>1. حقل بريد: <kbd>&lt;label for=\"email\"&gt;البريد:&lt;/label&gt;</kbd> و <kbd>&lt;input type=\"email\" id=\"email\"&gt;</kbd><br>2. حقل كلمة مرور: <kbd>&lt;label for=\"password\"&gt;كلمة المرور:&lt;/label&gt;</kbd> و <kbd>&lt;input type=\"password\" id=\"password\"&gt;</kbd>",
"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>&lt;label for=\"email\"&gt;Email:&lt;/label&gt;</kbd> and <kbd>&lt;input type=\"email\" id=\"email\"&gt;</kbd><br>2. A password field: <kbd>&lt;label for=\"password\"&gt;Password:&lt;/label&gt;</kbd> and <kbd>&lt;input type=\"password\" id=\"password\"&gt;</kbd>",
"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\">البريد:</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <label for=\"password\">كلمة المرور:</label>\n <input type=\"password\" id=\"password\" name=\"password\">\n</form>",
"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>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "input[type='email']",
"message": "أضف حقل بـ type=\"email\""
"message": "Add an input with type=\"email\""
},
{
"type": "element_exists",
"value": "input[type='password']",
"message": "أضف حقل بـ type=\"password\""
"message": "Add an input with type=\"password\""
},
{
"type": "element_count",
"value": { "selector": "label", "min": 2 },
"message": "أضف تسميات لكلا الحقلين"
"message": "Add labels for both inputs"
}
]
},
{
"id": "submit-button",
"title": "زر الإرسال",
"description": "النماذج تحتاج طريقة لإرسال البيانات. استخدم:<br><br><kbd>&lt;button type=\"submit\"&gt;</kbd> - مفضل، محتوى مرن<br><kbd>&lt;input type=\"submit\"&gt;</kbd> - زر نص بسيط<br><br>نص الزر يجب أن يكون موجه للعمل (مثل <code>تسجيل الدخول</code>، 'التسجيل'، 'إرسال').",
"task": "أضف زر إرسال للنموذج بالنص <code>تسجيل الدخول</code>.",
"title": "Submit Button",
"description": "Forms need a way to submit data. Use:<br><br><kbd>&lt;button type=\"submit\"&gt;</kbd> - Preferred, flexible content<br><kbd>&lt;input type=\"submit\"&gt;</kbd> - Simple text-only button<br><br>The button text should be action-oriented (e.g., <code>Sign In</code>, 'Register', 'Send').",
"task": "Add a submit button to the form with the text <code>Sign In</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\">البريد:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">كلمة المرور:</label>\n <input type=\"password\" id=\"password\">\n \n</form>",
"solution": "<form>\n <label for=\"email\">البريد:</label>\n <input type=\"email\" id=\"email\">\n \n <label for=\"password\">كلمة المرور:</label>\n <input type=\"password\" id=\"password\">\n \n <button type=\"submit\">تسجيل الدخول</button>\n</form>",
"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>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "button[type='submit'], input[type='submit']",
"message": "أضف زر إرسال لنموذجك"
"message": "Add a submit button to your form"
},
{
"type": "element_text",
"value": { "selector": "button", "text": "تسجيل الدخول" },
"message": "يجب أن يعرض الزر <kbd>تسجيل الدخول</kbd>"
"value": { "selector": "button", "text": "Sign In" },
"message": "The button should say <kbd>Sign In</kbd>"
}
]
}

View File

@@ -1,32 +1,110 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-validation",
"title": "تحقق النماذج",
"description": "استخدم تحقق HTML5 المدمج لتجربة مستخدم أفضل",
"title": "HTML Validation",
"description": "Learn HTML5 built-in form validation attributes",
"mode": "html",
"difficulty": "beginner",
"difficulty": "intermediate",
"lessons": [
{
"id": "required-fields",
"title": "الحقول المطلوبة",
"description": "سمة <kbd>required</kbd> تمنع إرسال النموذج إذا كان الحقل فارغاً. يعرض المتصفح رسالة تحقق تلقائياً - بدون JavaScript!<br><br>أضفها لأي حقل يجب ملؤه:<br><kbd>&lt;input type=\"text\" required&gt;</kbd>",
"task": "اجعل كلا الحقلين (الاسم والبريد) مطلوبين بإضافة سمة <kbd>required</kbd> لكل حقل.",
"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>&lt;input type=\"text\" required&gt;</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.",
"previewHTML": "",
"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; }",
"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; }",
"sandboxCSS": "",
"initialCode": "<form>\n <label for=\"name\">الاسم *</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n \n <label for=\"email\">البريد *</label>\n <input type=\"email\" id=\"email\" name=\"email\">\n \n <button type=\"submit\">إرسال</button>\n</form>",
"solution": "<form>\n <label for=\"name\">الاسم *</label>\n <input type=\"text\" id=\"name\" name=\"name\" required>\n \n <label for=\"email\">البريد *</label>\n <input type=\"email\" id=\"email\" name=\"email\" required>\n \n <button type=\"submit\">إرسال</button>\n</form>",
"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>",
"previewContainer": "preview-area",
"validations": [
{
"type": "attribute_value",
"value": { "selector": "input[name='name']", "attr": "required", "value": true },
"message": "أضف <kbd>required</kbd> لحقل الاسم"
"message": "Add the <kbd>required</kbd> attribute to the name input"
},
{
"type": "attribute_value",
"value": { "selector": "input[name='email']", "attr": "required", "value": true },
"message": "أضف <kbd>required</kbd> لحقل البريد"
"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>"
}
]
}

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-details-summary",
"title": "HTML Details & Summary",
"description": "أنشئ أقسام قابلة للتوسيع بدون JavaScript",
"description": "Create expandable content sections without JavaScript",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "details-summary-basic",
"title": "أول عنصر تفاعلي",
"description": "عنصر <kbd>&lt;details&gt;</kbd> ينشئ قسماً قابلاً للطي. عنصر <kbd>&lt;summary&gt;</kbd> يوفر التسمية القابلة للنقر.<br><br>انقر على الملخص لإظهار المحتوى المخفي - بدون JavaScript!",
"task": "أنشئ عنصر <kbd>&lt;details&gt;</kbd> مع:<br>1. عنصر <kbd>&lt;summary&gt;</kbd> يقول <code>Click to reveal</code><br>2. عنصر <kbd>&lt;p&gt;</kbd> بالنص <code>This content was hidden!</code>",
"title": "First Widget",
"description": "The <kbd>&lt;details&gt;</kbd> element creates a collapsible section. The <kbd>&lt;summary&gt;</kbd> provides the clickable label.<br><br>Click the summary to toggle the hidden content - no JavaScript needed!",
"task": "Create a <kbd>&lt;details&gt;</kbd> element with:<br>1. A <kbd>&lt;summary&gt;</kbd> saying <code>Click to reveal</code><br>2. A <kbd>&lt;p&gt;</kbd> with the text <code>This content was hidden!</code>",
"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": "أضف عنصر <kbd>&lt;details&gt;</kbd>"
"message": "Add a <kbd>&lt;details&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "summary",
"message": "أضف <kbd>&lt;summary&gt;</kbd> داخل details"
"message": "Add a <kbd>&lt;summary&gt;</kbd> inside the details"
},
{
"type": "parent_child",
"value": { "parent": "details", "child": "summary" },
"message": "يجب أن يكون <kbd>&lt;summary&gt;</kbd> داخل <kbd>&lt;details&gt;</kbd>"
"message": "The <kbd>&lt;summary&gt;</kbd> must be inside <kbd>&lt;details&gt;</kbd>"
},
{
"type": "parent_child",
"value": { "parent": "details", "child": "p" },
"message": "أضف <kbd>&lt;p&gt;</kbd> داخل <kbd>&lt;details&gt;</kbd> للمحتوى المخفي"
"message": "Add a <kbd>&lt;p&gt;</kbd> inside <kbd>&lt;details&gt;</kbd> for the hidden content"
}
]
},
{
"id": "details-open-attribute",
"title": "موسع افتراضياً",
"description": "افتراضياً، <kbd>&lt;details&gt;</kbd> مغلق. أضف سمة <kbd>open</kbd> لإظهار المحتوى في البداية.<br><br>هذه سمة منطقية - فقط أضف <kbd>open</kbd> بدون قيمة.",
"task": "أضف سمة <kbd>open</kbd> لعنصر <kbd>&lt;details&gt;</kbd> لإظهار المحتوى افتراضياً.",
"title": "Pre-expanded Details",
"description": "By default, <kbd>&lt;details&gt;</kbd> is closed. Add the <kbd>open</kbd> attribute to show the content initially.<br><br>This is a boolean attribute - just add <kbd>open</kbd> without a value.",
"task": "Add the <kbd>open</kbd> attribute to the <kbd>&lt;details&gt;</kbd> element to show the content by default.",
"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": "أضف سمة <kbd>open</kbd> إلى <kbd>&lt;details&gt;</kbd>"
"message": "Add the <kbd>open</kbd> attribute to <kbd>&lt;details&gt;</kbd>"
}
]
},
{
"id": "faq-accordion",
"title": "أكورديون FAQ",
"description": "عناصر <kbd>&lt;details&gt;</kbd> المتعددة تنشئ FAQ بأسلوب الأكورديون. كل سؤال يمكن توسيعه بشكل مستقل.<br><br><b>نصيحة:</b> اكتب <kbd>details*3&gt;summary+p</kbd> واضغط Tab لتوسيع Emmet. <kbd>*3</kbd> ينشئ 3 عناصر، <kbd>&gt;</kbd> يضع بالداخل، <kbd>+</kbd> يضيف أشقاء.",
"task": "أنشئ قسم FAQ مع:<br>1. عنصر <kbd>&lt;h1&gt;</kbd> يقول <code>Frequently Asked Questions</code><br>2. ثلاثة عناصر <kbd>&lt;details&gt;</kbd>، كل واحد بسؤال في <kbd>&lt;summary&gt;</kbd> وإجابة في <kbd>&lt;p&gt;</kbd>",
"title": "FAQ Accordion",
"description": "Multiple <kbd>&lt;details&gt;</kbd> elements create an accordion-style FAQ. Each question can be expanded independently.<br><br><b>Pro tip:</b> Type <kbd>details*3&gt;summary+p</kbd> and press Tab for Emmet expansion. <kbd>*3</kbd> creates 3 elements, <kbd>&gt;</kbd> nests inside, <kbd>+</kbd> adds siblings.",
"task": "Create an FAQ section with:<br>1. An <kbd>&lt;h1&gt;</kbd> saying <code>Frequently Asked Questions</code><br>2. Three <kbd>&lt;details&gt;</kbd> elements, each with a question in <kbd>&lt;summary&gt;</kbd> and an answer in <kbd>&lt;p&gt;</kbd>",
"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": "أضف عنوان <kbd>&lt;h1&gt;</kbd> لعنوان FAQ"
"message": "Add an <kbd>&lt;h1&gt;</kbd> heading for the FAQ title"
},
{
"type": "element_count",
"value": { "selector": "details", "min": 3 },
"message": "أنشئ على الأقل 3 عناصر <kbd>&lt;details&gt;</kbd> للـ FAQ"
"message": "Create at least 3 <kbd>&lt;details&gt;</kbd> elements for the FAQ"
},
{
"type": "element_count",
"value": { "selector": "summary", "min": 3 },
"message": "كل <kbd>&lt;details&gt;</kbd> يحتاج <kbd>&lt;summary&gt;</kbd> للسؤال"
"message": "Each <kbd>&lt;details&gt;</kbd> needs a <kbd>&lt;summary&gt;</kbd> for the question"
},
{
"type": "element_count",
"value": { "selector": "details p", "min": 3 },
"message": "كل <kbd>&lt;details&gt;</kbd> يحتاج <kbd>&lt;p&gt;</kbd> للإجابة"
"message": "Each <kbd>&lt;details&gt;</kbd> needs a <kbd>&lt;p&gt;</kbd> for the answer"
}
]
}

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-progress-meter",
"title": "HTML Progress & Meter",
"description": "اعرض حالة الإكمال والقياسات بشكل أصلي",
"description": "Display completion status and scalar measurements natively",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "progress-basic",
"title": "أشرطة التقدم",
"description": "عنصر <kbd>&lt;progress&gt;</kbd> يُظهر إكمال المهمة. استخدم <kbd>value</kbd> للتقدم الحالي و <kbd>max</kbd> للإجمالي.<br><br><b>ملاحظة:</b> هذا ليس وسماً ذاتي الإغلاق! اكتب <kbd>&lt;progress&gt;...&lt;/progress&gt;</kbd> مع نص بديل بالداخل للمتصفحات القديمة.",
"task": "أنشئ شريط تقدم يُظهر 70% إكمال:<br>1. أضف <kbd>&lt;label&gt;</kbd> يقول <code>Download:</code><br>2. أضف <kbd>&lt;progress&gt;</kbd> مع <kbd>value=\"70\"</kbd> و <kbd>max=\"100\"</kbd>",
"title": "Progress Bars",
"description": "The <kbd>&lt;progress&gt;</kbd> element shows task completion. Use <kbd>value</kbd> for current progress and <kbd>max</kbd> for the total.<br><br><b>Note:</b> This is not a self-closing tag! Write <kbd>&lt;progress&gt;...&lt;/progress&gt;</kbd> with fallback text inside for older browsers.",
"task": "Create a progress bar showing 70% completion:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Download:</code><br>2. Add a <kbd>&lt;progress&gt;</kbd> with <kbd>value=\"70\"</kbd> and <kbd>max=\"100\"</kbd>",
"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": "أضف عنصر <kbd>&lt;progress&gt;</kbd>"
"message": "Add a <kbd>&lt;progress&gt;</kbd> element"
},
{
"type": "attribute_value",
"value": { "selector": "progress", "attr": "value", "value": "70" },
"message": "عيّن <kbd>value=</kbd>\"70\" في عنصر progress"
"message": "Set <kbd>value=</kbd>\"70\" on the progress element"
},
{
"type": "attribute_value",
"value": { "selector": "progress", "attr": "max", "value": "100" },
"message": "عيّن <kbd>max=</kbd>\"100\" في عنصر progress"
"message": "Set <kbd>max=</kbd>\"100\" on the progress element"
},
{
"type": "element_exists",
"value": "label",
"message": "أضف <kbd>&lt;label&gt;</kbd> لشريط التقدم"
"message": "Add a <kbd>&lt;label&gt;</kbd> for the progress bar"
}
]
},
{
"id": "progress-indeterminate",
"title": "تقدم غير محدد",
"description": "عندما يكون التقدم غير معروف (مثل التحميل)، احذف سمة <kbd>value</kbd>. هذا ينشئ حالة متحركة غير محددة.<br><br>مفيد لطلبات الشبكة أو العمليات ذات المدة غير المعروفة.",
"task": "أنشئ مؤشر تحميل:<br>1. أضف <kbd>&lt;p&gt;</kbd> يقول <code>Loading...</code><br>2. أضف <kbd>&lt;progress&gt;</kbd> بدون سمة value",
"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>&lt;p&gt;</kbd> saying <code>Loading...</code><br>2. Add a <kbd>&lt;progress&gt;</kbd> without a value attribute",
"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": "أضف عنصر <kbd>&lt;progress&gt;</kbd>"
"message": "Add a <kbd>&lt;progress&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "p",
"message": "أضف <kbd>&lt;p&gt;</kbd> مع نص التحميل"
"message": "Add a <kbd>&lt;p&gt;</kbd> with loading text"
}
]
},
{
"id": "meter-gauge",
"title": "مقاييس meter",
"description": "عنصر <kbd>&lt;meter&gt;</kbd> يعرض قيمة قياسية ضمن نطاق. استخدمه للقياسات مثل مساحة القرص، البطارية، أو التقييمات.<br><br>عيّن <kbd>low</kbd> و <kbd>high</kbd> و <kbd>optimum</kbd> لتحديد النطاقات الجيدة/السيئة - المتصفح يلونها وفقاً لذلك!",
"task": "أنشئ مقياس مستوى البطارية:<br>1. أضف <kbd>&lt;label&gt;</kbd> يقول <code>Battery:</code><br>2. أضف <kbd>&lt;meter&gt;</kbd> مع:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> و <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> و <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>",
"title": "Meter Gauges",
"description": "The <kbd>&lt;meter&gt;</kbd> element displays a scalar value within a range. Use it for measurements like disk space, battery, or ratings.<br><br>Set <kbd>low</kbd>, <kbd>high</kbd>, and <kbd>optimum</kbd> to define good/bad ranges - the browser colors it accordingly!",
"task": "Create a battery level meter:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Battery:</code><br>2. Add a <kbd>&lt;meter&gt;</kbd> with:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> and <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> and <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>",
"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,42 +79,22 @@
{
"type": "element_exists",
"value": "meter",
"message": "أضف عنصر <kbd>&lt;meter&gt;</kbd>"
"message": "Add a <kbd>&lt;meter&gt;</kbd> element"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "value", "value": "0.8" },
"message": "عيّن <kbd>value=</kbd>\"0.8\" في meter"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "min", "value": "0" },
"message": "عيّن <kbd>min=</kbd>\"0\" في meter"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "max", "value": "1" },
"message": "عيّن <kbd>max=</kbd>\"1\" في meter"
"message": "Set <kbd>value=</kbd>\"0.8\" on the meter"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "low", "value": "0.2" },
"message": "عيّن <kbd>low=</kbd>\"0.2\" لتحديد العتبة المنخفضة"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "high", "value": "0.8" },
"message": "عيّن <kbd>high=</kbd>\"0.8\" لتحديد العتبة العالية"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "optimum", "value": "1" },
"message": "عيّن <kbd>optimum=</kbd>\"1\" للإشارة إلى القيمة المثلى"
"message": "Set <kbd>low=</kbd>\"0.2\" to define the low threshold"
},
{
"type": "element_exists",
"value": "label",
"message": "أضف <kbd>&lt;label&gt;</kbd> للـ meter"
"message": "Add a <kbd>&lt;label&gt;</kbd> for the meter"
}
]
}

View File

@@ -1,16 +1,16 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-datalist",
"title": "قائمة البيانات",
"description": "قدم اقتراحات لحقول النص بدون JavaScript",
"title": "Datalist",
"description": "Provide suggestions for text inputs without JavaScript",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "datalist-basic",
"title": "حقل مع اقتراحات",
"description": "عنصر <kbd>&lt;datalist&gt;</kbd> يوفر اقتراحات الإكمال التلقائي للحقول. اربطه باستخدام سمة <kbd>list</kbd> في الحقل بما يتطابق مع <kbd>id</kbd> قائمة البيانات.<br><br>يمكن للمستخدمين الكتابة بحرية - الاقتراحات مجرد مساعدات!",
"task": "أنشئ محدد متصفح:<br>1. أضف <kbd>&lt;label&gt;</kbd> يقول <code>المتصفح:</code><br>2. أضف <kbd>&lt;input&gt;</kbd> مع <kbd>list=\"browsers\"</kbd><br>3. أضف <kbd>&lt;datalist id=\"browsers\"&gt;</kbd> مع خيارات Chrome و Firefox و Safari",
"title": "Input with Suggestions",
"description": "The <kbd>&lt;datalist&gt;</kbd> element provides autocomplete suggestions for inputs. Connect it using the <kbd>list</kbd> attribute on the input matching the datalist's <kbd>id</kbd>.<br><br>Users can still type freely - suggestions are just helpers!",
"task": "Create a browser selector:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Browser:</code><br>2. Add an <kbd>&lt;input&gt;</kbd> with <kbd>list=\"browsers\"</kbd><br>3. Add a <kbd>&lt;datalist id=\"browsers\"&gt;</kbd> with options for Chrome, Firefox, and Safari",
"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": "أضف عنصر <kbd>&lt;datalist&gt;</kbd>"
"message": "Add a <kbd>&lt;datalist&gt;</kbd> element"
},
{
"type": "attribute_value",
"value": { "selector": "input", "attr": "list", "value": "browsers" },
"message": "اربط الحقل بقائمة البيانات باستخدام <kbd>list=</kbd>\"browsers\""
"message": "Connect the input to datalist using <kbd>list=</kbd>\"browsers\""
},
{
"type": "element_count",
"value": { "selector": "option", "min": 3 },
"message": "أضف على الأقل 3 عناصر <kbd>&lt;option&gt;</kbd> داخل <kbd>&lt;datalist&gt;</kbd>"
"message": "Add at least 3 <kbd>&lt;option&gt;</kbd> elements inside <kbd>&lt;datalist&gt;</kbd>"
},
{
"type": "element_exists",
"value": "label",
"message": "أضف <kbd>&lt;label&gt;</kbd> للحقل"
"message": "Add a <kbd>&lt;label&gt;</kbd> for the input"
}
]
},
{
"id": "datalist-countries",
"title": "محدد الدول",
"description": "قوائم البيانات تعمل بشكل رائع للقوائم الطويلة مثل الدول. يمكن للمستخدمين الكتابة لتصفية الاقتراحات فوراً.<br><br>سمة <kbd>value</kbd> هي ما يتم إدخاله، ويمكنك إضافة نص عرض بعدها.",
"task": "أنشئ حقل دولة:<br>1. أضف <kbd>&lt;label&gt;</kbd> يقول <code>الدولة:</code><br>2. أضف <kbd>&lt;input&gt;</kbd> مع <kbd>list=\"countries\"</kbd><br>3. أضف <kbd>&lt;datalist id=\"countries\"&gt;</kbd> مع 4 خيارات دول على الأقل",
"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>&lt;label&gt;</kbd> saying <code>Country:</code><br>2. Add an <kbd>&lt;input&gt;</kbd> with <kbd>list=\"countries\"</kbd><br>3. Add a <kbd>&lt;datalist id=\"countries\"&gt;</kbd> with at least 4 country options",
"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": "أضف عنصر <kbd>&lt;datalist&gt;</kbd>"
"message": "Add a <kbd>&lt;datalist&gt;</kbd> element"
},
{
"type": "attribute_value",
"value": { "selector": "datalist", "attr": "id", "value": "countries" },
"message": "عيّن <kbd>id=</kbd>\"countries\" في قائمة البيانات"
"message": "Set <kbd>id=</kbd>\"countries\" on the datalist"
},
{
"type": "attribute_value",
"value": { "selector": "input", "attr": "list", "value": "countries" },
"message": "اربط الحقل باستخدام <kbd>list=</kbd>\"countries\""
"message": "Connect the input using <kbd>list=</kbd>\"countries\""
},
{
"type": "element_count",
"value": { "selector": "option", "min": 4 },
"message": "أضف على الأقل 4 خيارات دول"
"message": "Add at least 4 country options"
}
]
}

View File

@@ -1,16 +1,16 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-dialog",
"title": "مربعات الحوار",
"description": "أنشئ مربعات حوار نموذجية بدون مكتبات JavaScript",
"title": "Dialogs",
"description": "Create modal dialogs without JavaScript libraries",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "dialog-basic",
"title": "فتح مربع الحوار",
"description": "عنصر <kbd>&lt;dialog&gt;</kbd> ينشئ نافذة نموذجية أصلية. أضف سمة <kbd>open</kbd> لعرضها.<br><br>استخدم <kbd>&lt;form method=\"dialog\"&gt;</kbd> بداخلها لإغلاقها عند إرسال النموذج - بدون JavaScript!",
"task": "أنشئ مربع حوار مع:<br>1. سمة <kbd>open</kbd> لعرضه<br>2. عنصر <kbd>&lt;h2&gt;</kbd> يقول <code>أهلاً!</code><br>3. عنصر <kbd>&lt;p&gt;</kbd> مع رسالة ترحيب<br>4. عنصر <kbd>&lt;form method=\"dialog\"&gt;</kbd> مع زر إغلاق",
"title": "Open Dialog",
"description": "The <kbd>&lt;dialog&gt;</kbd> element creates a native modal. Add the <kbd>open</kbd> attribute to show it.<br><br>Use <kbd>&lt;form method=\"dialog\"&gt;</kbd> inside to close it when the form submits - no JavaScript needed!",
"task": "Create a dialog with:<br>1. The <kbd>open</kbd> attribute to show it<br>2. An <kbd>&lt;h2&gt;</kbd> saying <code>Welcome!</code><br>3. A <kbd>&lt;p&gt;</kbd> with a greeting message<br>4. A <kbd>&lt;form method=\"dialog\"&gt;</kbd> with a close button",
"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": "أضف عنصر <kbd>&lt;dialog&gt;</kbd>"
"message": "Add a <kbd>&lt;dialog&gt;</kbd> element"
},
{
"type": "attribute_value",
"value": { "selector": "dialog", "attr": "open", "value": true },
"message": "أضف سمة <kbd>open</kbd> لعرض مربع الحوار"
"message": "Add the <kbd>open</kbd> attribute to show the dialog"
},
{
"type": "element_exists",
"value": "dialog h2",
"message": "أضف عنوان <kbd>&lt;h2&gt;</kbd> داخل مربع الحوار"
"message": "Add an <kbd>&lt;h2&gt;</kbd> heading inside the dialog"
},
{
"type": "element_exists",
"value": "form[method='dialog']",
"message": "أضف <kbd>&lt;form method=\"dialog\"&gt;</kbd> للإغلاق"
"message": "Add a <kbd>&lt;form method=\"dialog\"&gt;</kbd> for closing"
},
{
"type": "element_exists",
"value": "dialog button",
"message": "أضف زر إغلاق داخل النموذج"
"message": "Add a close button inside the form"
}
]
},
{
"id": "dialog-form",
"title": "حوار + نموذج",
"description": "مربعات الحوار يمكن أن تحتوي على نماذج كاملة. <kbd>method=\"dialog\"</kbd> يجعل النموذج يغلق مربع الحوار عند الإرسال بدلاً من إرسال البيانات.<br><br>هذا النمط مثالي لحوارات التأكيد، المدخلات السريعة، أو لوحات الإعدادات.",
"task": "أنشئ مربع حوار تأكيد:<br>1. أضف <kbd>open</kbd> لعرضه<br>2. عنصر <kbd>&lt;h2&gt;</kbd> يقول <code>تأكيد الحذف</code><br>3. عنصر <kbd>&lt;p&gt;</kbd> يسأل <code>هل أنت متأكد؟</code><br>4. عنصر <kbd>&lt;form method=\"dialog\"&gt;</kbd> مع أزرار إلغاء وحذف",
"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>&lt;h2&gt;</kbd> saying <code>Confirm Delete</code><br>3. A <kbd>&lt;p&gt;</kbd> asking <code>Are you sure?</code><br>4. A <kbd>&lt;form method=\"dialog\"&gt;</kbd> with Cancel and Delete buttons",
"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": "أضف <kbd>&lt;dialog&gt;</kbd> مع سمة open"
"message": "Add a <kbd>&lt;dialog&gt;</kbd> with the open attribute"
},
{
"type": "element_exists",
"value": "dialog h2",
"message": "أضف عنواناً لمربع الحوار"
"message": "Add a heading to the dialog"
},
{
"type": "element_exists",
"value": "form[method='dialog']",
"message": "أضف <kbd>&lt;form method=\"dialog\"&gt;</kbd> للأزرار"
"message": "Add a <kbd>&lt;form method=\"dialog\"&gt;</kbd> for the buttons"
},
{
"type": "element_count",
"value": { "selector": "dialog button", "min": 2 },
"message": "أضف على الأقل زرين (إلغاء وتأكيد)"
"message": "Add at least 2 buttons (Cancel and Confirm)"
}
]
}

View File

@@ -1,16 +1,16 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-fieldset",
"title": "مجموعة الحقول",
"description": "جمّع عناصر التحكم في النموذج باستخدام fieldset و legend",
"title": "Fieldsets",
"description": "Group form controls with fieldset and legend elements",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "fieldset-basic",
"title": "التجميع مع Fieldset",
"description": "عنصر <kbd>&lt;fieldset&gt;</kbd> يجمع عناصر التحكم المرتبطة في النموذج معاً. أضف <kbd>&lt;legend&gt;</kbd> كأول عنصر فرعي لإعطاء المجموعة عنواناً.<br><br>هذا يساعد في إمكانية الوصول والتنظيم البصري للنماذج المعقدة.",
"task": "أنشئ نموذجاً مع fieldset:<br>1. عنصر <kbd>&lt;form&gt;</kbd><br>2. عنصر <kbd>&lt;fieldset&gt;</kbd> بداخله<br>3. عنصر <kbd>&lt;legend&gt;</kbd> يقول <code>المعلومات الشخصية</code><br>4. حقلين مُعنونين للاسم والبريد الإلكتروني",
"title": "Grouping with Fieldset",
"description": "The <kbd>&lt;fieldset&gt;</kbd> element groups related form controls together. Add a <kbd>&lt;legend&gt;</kbd> as the first child to give the group a title.<br><br>This helps with accessibility and visual organization of complex forms.",
"task": "Create a form with a fieldset:<br>1. A <kbd>&lt;form&gt;</kbd> element<br>2. A <kbd>&lt;fieldset&gt;</kbd> inside<br>3. A <kbd>&lt;legend&gt;</kbd> saying <code>Personal Info</code><br>4. Two labeled inputs for name and email",
"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": "أضف عنصر <kbd>&lt;form&gt;</kbd>"
"message": "Add a <kbd>&lt;form&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "fieldset",
"message": "أضف <kbd>&lt;fieldset&gt;</kbd> داخل النموذج"
"message": "Add a <kbd>&lt;fieldset&gt;</kbd> inside the form"
},
{
"type": "element_exists",
"value": "legend",
"message": "أضف <kbd>&lt;legend&gt;</kbd> لعنونة مجموعة الحقول"
"message": "Add a <kbd>&lt;legend&gt;</kbd> to title your fieldset"
},
{
"type": "element_count",
"value": { "selector": "label", "min": 2 },
"message": "أضف على الأقل عنوانين"
"message": "Add at least 2 labels"
},
{
"type": "element_count",
"value": { "selector": "input", "min": 2 },
"message": "أضف على الأقل حقلي إدخال"
"message": "Add at least 2 input fields"
}
]
},
{
"id": "fieldset-textarea",
"title": "إضافة Textarea",
"description": "عنصر <kbd>&lt;textarea&gt;</kbd> ينشئ حقل إدخال نص متعدد الأسطر، مثالي للمحتوى الطويل مثل الرسائل أو الأوصاف.<br><br>استخدم سمات <kbd>rows</kbd> و <kbd>cols</kbd> لتعيين الحجم الافتراضي.",
"task": "أنشئ نموذج اتصال:<br>1. عنصر <kbd>&lt;fieldset&gt;</kbd> مع <kbd>&lt;legend&gt;</kbd> <code>تواصل معنا</code><br>2. حقل <kbd>&lt;input&gt;</kbd> مُعنون للبريد الإلكتروني<br>3. حقل <kbd>&lt;textarea&gt;</kbd> مُعنون للرسالة<br>4. زر <kbd>&lt;button&gt;</kbd> للإرسال",
"title": "Adding Textarea",
"description": "The <kbd>&lt;textarea&gt;</kbd> element creates a multi-line text input, perfect for longer content like messages or descriptions.<br><br>Use <kbd>rows</kbd> and <kbd>cols</kbd> attributes to set default size.",
"task": "Create a contact form:<br>1. A <kbd>&lt;fieldset&gt;</kbd> with <kbd>&lt;legend&gt;</kbd> <code>Contact Us</code><br>2. A labeled <kbd>&lt;input&gt;</kbd> for email<br>3. A labeled <kbd>&lt;textarea&gt;</kbd> for the message<br>4. A submit <kbd>&lt;button&gt;</kbd>",
"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": "أضف عنصر <kbd>&lt;fieldset&gt;</kbd>"
"message": "Add a <kbd>&lt;fieldset&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "legend",
"message": "أضف عنصر <kbd>&lt;legend&gt;</kbd>"
"message": "Add a <kbd>&lt;legend&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "textarea",
"message": "أضف <kbd>&lt;textarea&gt;</kbd> للرسالة"
"message": "Add a <kbd>&lt;textarea&gt;</kbd> for the message"
},
{
"type": "element_exists",
"value": "button",
"message": "أضف زر إرسال"
"message": "Add a submit button"
},
{
"type": "element_exists",
"value": "input",
"message": "أضف حقل للبريد الإلكتروني"
"message": "Add an input field for email"
}
]
},
{
"id": "fieldset-multiple",
"title": "مجموعات حقول متعددة",
"description": "النماذج المعقدة يمكنها استخدام عناصر <kbd>&lt;fieldset&gt;</kbd> متعددة لتنظيم أقسام مختلفة.<br><br>هذا يحسن قابلية الاستخدام للنماذج الطويلة مثل التسجيل أو الدفع.",
"task": "أنشئ نموذج تسجيل مع 2 مجموعات حقول:<br>1. <code>معلومات الحساب</code> مع حقول اسم المستخدم وكلمة المرور<br>2. <code>التفضيلات</code> مع textarea للسيرة الذاتية<br>3. زر إرسال خارج مجموعات الحقول",
"title": "Multiple Fieldsets",
"description": "Complex forms can use multiple <kbd>&lt;fieldset&gt;</kbd> elements to organize different sections.<br><br>This improves usability for long forms like registration or checkout pages.",
"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",
"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": "أنشئ على الأقل 2 مجموعات حقول"
"message": "Create at least 2 fieldsets"
},
{
"type": "element_count",
"value": { "selector": "legend", "min": 2 },
"message": "أضف legend لكل مجموعة حقول"
"message": "Add a legend to each fieldset"
},
{
"type": "element_exists",
"value": "textarea",
"message": "أضف textarea للسيرة الذاتية"
"message": "Add a textarea for the bio"
},
{
"type": "element_exists",
"value": "button",
"message": "أضف زر إرسال"
"message": "Add a submit button"
},
{
"type": "element_count",
"value": { "selector": "input", "min": 2 },
"message": "أضف على الأقل حقلي إدخال"
"message": "Add at least 2 input fields"
}
]
}

View File

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

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-marquee",
"title": "HTML Marquee",
"description": "أنشئ نصاً متحركاً باستخدام عنصر marquee الكلاسيكي (قديم لكنه ممتع!)",
"description": "Create scrolling text with the classic (deprecated but fun!) marquee element",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "marquee-basic",
"title": "النص المتحرك",
"description": "عنصر <kbd>&lt;marquee&gt;</kbd> ينشئ نصاً متحركاً - كلاسيكي من الويب القديم! رغم أنه قديم، لا يزال يعمل في معظم المتصفحات.<br><br>ملاحظة: للإنتاج، استخدم حركات CSS بدلاً منه. لكن للتعلم والمرح، marquee رائع!",
"task": "أنشئ marquee بسيط:<br>1. أضف عنصر <kbd>&lt;marquee&gt;</kbd><br>2. ضع نصاً بداخله مثل <code>مرحباً بك في موقعي!</code>",
"title": "Scrolling Text",
"description": "The <kbd>&lt;marquee&gt;</kbd> element creates scrolling text - a classic from the early web! While deprecated, it still works in most browsers.<br><br>Note: For production, use CSS animations instead. But for learning and fun, marquee is great!",
"task": "Create a simple marquee:<br>1. Add a <kbd>&lt;marquee&gt;</kbd> element<br>2. Put some text inside like <code>Welcome to my website!</code>",
"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": "أضف عنصر <kbd>&lt;marquee&gt;</kbd>"
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element"
}
]
},
{
"id": "marquee-direction",
"title": "الاتجاه والسلوك",
"description": "تحكم في marquee باستخدام السمات:<br>• <kbd>direction</kbd>: left، right، up، down<br>• <kbd>behavior</kbd>: scroll (افتراضي)، slide (يتوقف عند الحافة)، alternate (يرتد)<br>• <kbd>scrollamount</kbd>: السرعة (الافتراضي 6)",
"task": "أنشئ marquee مرتداً:<br>1. أضف عنصر <kbd>&lt;marquee&gt;</kbd><br>2. اضبط <kbd>behavior=\"alternate\"</kbd> ليرتد<br>3. أضف نصاً ممتعاً",
"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>&lt;marquee&gt;</kbd> element<br>2. Set <kbd>behavior=\"alternate\"</kbd> to make it bounce<br>3. Add some fun text",
"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": "أضف عنصر <kbd>&lt;marquee&gt;</kbd>"
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element"
},
{
"type": "attribute_value",
"value": { "selector": "marquee", "attr": "behavior", "value": "alternate" },
"message": "أضف <kbd>behavior=</kbd>\"alternate\" ليرتد"
"message": "Add <kbd>behavior=</kbd>\"alternate\" to make it bounce"
}
]
},
{
"id": "marquee-retro",
"title": "شريط أخبار كلاسيكي",
"description": "اجمع عدة سمات marquee لتأثير شريط أخبار كلاسيكي. يمكنك حتى وضع عناصر متعددة بداخله!<br><br>تذكر: هذا HTML قديم. المواقع الحديثة تستخدم حركات CSS، لكن marquee رائع لفهم تاريخ الويب.",
"task": "أنشئ شريط أخبار:<br>1. عنصر <kbd>&lt;marquee&gt;</kbd> مع <kbd>direction=\"left\"</kbd><br>2. اضبط <kbd>scrollamount=\"5\"</kbd> للتمرير السلس<br>3. أضف عنواناً إخبارياً عاجلاً بداخله",
"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>&lt;marquee&gt;</kbd> with <kbd>direction=\"left\"</kbd><br>2. Set <kbd>scrollamount=\"5\"</kbd> for smooth scrolling<br>3. Add a breaking news headline inside",
"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": "أضف عنصر <kbd>&lt;marquee&gt;</kbd>"
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element"
},
{
"type": "attribute_value",
"value": { "selector": "marquee", "attr": "direction", "value": "left" },
"message": "أضف <kbd>direction=</kbd>\"left\" للتمرير الأفقي"
"message": "Add <kbd>direction=</kbd>\"left\" for horizontal scrolling"
},
{
"type": "attribute_value",
"value": { "selector": "marquee", "attr": "scrollamount", "value": "5" },
"message": "أضف <kbd>scrollamount=</kbd>\"5\" لسرعة سلسة"
"message": "Add <kbd>scrollamount=</kbd>\"5\" for smooth speed"
}
]
}

View File

@@ -2,169 +2,99 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-svg",
"title": "HTML SVG",
"description": "ارسم رسومات متجهة قابلة للتحجيم مباشرة في HTML",
"description": "Draw scalable vector graphics directly in HTML",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "svg-circle",
"title": "رسم الدوائر",
"description": "SVG (Scalable Vector Graphics) يتيح لك رسم الأشكال مباشرة في HTML. عنصر <kbd>&lt;svg&gt;</kbd> هو الحاوية بسمات <kbd>width</kbd> و <kbd>height</kbd>.<br><br>استخدم <kbd>&lt;circle&gt;</kbd> مع <kbd>cx</kbd> و <kbd>cy</kbd> (المركز) و <kbd>r</kbd> (نصف القطر) لرسم الدوائر.",
"task": "أنشئ SVG مع دائرة:<br>1. عنصر <kbd>&lt;svg&gt;</kbd> بـ width=\"200\" و height=\"200\"<br>2. عنصر <kbd>&lt;circle&gt;</kbd> متمركز عند (100,100) بنصف قطر 50<br>3. أضف لون <kbd>fill</kbd>",
"title": "Drawing Circles",
"description": "SVG (Scalable Vector Graphics) lets you draw shapes directly in HTML. The <kbd>&lt;svg&gt;</kbd> element is the container, with <kbd>width</kbd> and <kbd>height</kbd> attributes.<br><br>Use <kbd>&lt;circle&gt;</kbd> with <kbd>cx</kbd>, <kbd>cy</kbd> (center) and <kbd>r</kbd> (radius) to draw circles.",
"task": "Create an SVG with a circle:<br>1. An <kbd>&lt;svg&gt;</kbd> with width=\"200\" and height=\"200\"<br>2. A <kbd>&lt;circle&gt;</kbd> centered at (100,100) with radius 50<br>3. Add a <kbd>fill</kbd> color",
"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=\"steelblue\" />\n</svg>",
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"#3498db\" />\n</svg>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "svg",
"message": "أضف عنصر <kbd>&lt;svg&gt;</kbd>"
"message": "Add an <kbd>&lt;svg&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "circle",
"message": "أضف عنصر <kbd>&lt;circle&gt;</kbd> داخل SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "width", "value": "200" },
"message": "عيّن <kbd>width=</kbd>\"200\" في SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "height", "value": "200" },
"message": "عيّن <kbd>height=</kbd>\"200\" في SVG"
"message": "Add a <kbd>&lt;circle&gt;</kbd> element inside the SVG"
},
{
"type": "attribute_value",
"value": { "selector": "circle", "attr": "cx", "value": "100" },
"message": "عيّن <kbd>cx=</kbd>\"100\" للمركز الأفقي للدائرة"
"message": "Set <kbd>cx=</kbd>\"100\" for the circle's horizontal center"
},
{
"type": "attribute_value",
"value": { "selector": "circle", "attr": "cy", "value": "100" },
"message": "عيّن <kbd>cy=</kbd>\"100\" للمركز العمودي للدائرة"
},
{
"type": "attribute_value",
"value": { "selector": "circle", "attr": "r", "value": "50" },
"message": "عيّن <kbd>r=</kbd>\"50\" لنصف قطر الدائرة"
"message": "Set <kbd>cy=</kbd>\"100\" for the circle's vertical center"
}
]
},
{
"id": "svg-rect-line",
"title": "المستطيلات والخطوط",
"description": "ارسم المستطيلات باستخدام <kbd>&lt;rect&gt;</kbd> مع <kbd>x</kbd> و <kbd>y</kbd> و <kbd>width</kbd> و <kbd>height</kbd>.<br><br>ارسم الخطوط باستخدام <kbd>&lt;line&gt;</kbd> مع <kbd>x1</kbd> و <kbd>y1</kbd> (البداية) و <kbd>x2</kbd> و <kbd>y2</kbd> (النهاية). الخطوط تحتاج لون <kbd>stroke</kbd>!",
"task": "أنشئ SVG مع:<br>1. عنصر <kbd>&lt;svg&gt;</kbd> (200x150)<br>2. عنصر <kbd>&lt;rect&gt;</kbd> في الموضع (20,20) بحجم 80x60<br>3. عنصر <kbd>&lt;line&gt;</kbd> من (120,30) إلى (180,90) مع لون stroke",
"title": "Rectangles & Lines",
"description": "Draw rectangles with <kbd>&lt;rect&gt;</kbd> using <kbd>x</kbd>, <kbd>y</kbd>, <kbd>width</kbd>, <kbd>height</kbd>.<br><br>Draw lines with <kbd>&lt;line&gt;</kbd> using <kbd>x1</kbd>, <kbd>y1</kbd> (start) and <kbd>x2</kbd>, <kbd>y2</kbd> (end). Lines need a <kbd>stroke</kbd> color!",
"task": "Create an SVG with:<br>1. An <kbd>&lt;svg&gt;</kbd> (200x150)<br>2. A <kbd>&lt;rect&gt;</kbd> at position (20,20) with size 80x60<br>3. A <kbd>&lt;line&gt;</kbd> from (120,30) to (180,90) with a stroke color",
"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=\"tomato\" />\n <line x1=\"120\" y1=\"30\" x2=\"180\" y2=\"90\" stroke=\"slategray\" stroke-width=\"3\" />\n</svg>",
"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>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "svg",
"message": "أضف عنصر <kbd>&lt;svg&gt;</kbd>"
"message": "Add an <kbd>&lt;svg&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "rect",
"message": "أضف عنصر <kbd>&lt;rect&gt;</kbd>"
"message": "Add a <kbd>&lt;rect&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "line",
"message": "أضف عنصر <kbd>&lt;line&gt;</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "width", "value": "200" },
"message": "عيّن <kbd>width=</kbd>\"200\" في SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "height", "value": "150" },
"message": "عيّن <kbd>height=</kbd>\"150\" في SVG"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "x", "value": "20" },
"message": "عيّن <kbd>x=</kbd>\"20\" في rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "y", "value": "20" },
"message": "عيّن <kbd>y=</kbd>\"20\" في rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "width", "value": "80" },
"message": "عيّن <kbd>width=</kbd>\"80\" في rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "height", "value": "60" },
"message": "عيّن <kbd>height=</kbd>\"60\" في rect"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "x1", "value": "120" },
"message": "عيّن <kbd>x1=</kbd>\"120\" في line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "y1", "value": "30" },
"message": "عيّن <kbd>y1=</kbd>\"30\" في line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "x2", "value": "180" },
"message": "عيّن <kbd>x2=</kbd>\"180\" في line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "y2", "value": "90" },
"message": "عيّن <kbd>y2=</kbd>\"90\" في line"
},
{
"type": "contains",
"value": "stroke",
"message": "أضف لون <kbd>stroke</kbd> إلى line"
"message": "Add a <kbd>&lt;line&gt;</kbd> element"
}
]
},
{
"id": "svg-shapes",
"title": "أشكال متعددة",
"description": "ادمج الأشكال لإنشاء رسومات بسيطة! أضف <kbd>stroke</kbd> للحدود و <kbd>stroke-width</kbd> للسمك.<br><br>استخدم <kbd>fill=\"none\"</kbd> للأشكال المجوفة. الأشكال تتراكم بالترتيب - العناصر اللاحقة تظهر في الأعلى.",
"task": "أنشئ وجهاً بسيطاً:<br>1. عنصر <kbd>&lt;svg&gt;</kbd> (200x200)<br>2. <kbd>&lt;circle&gt;</kbd> كبير للوجه<br>3. اثنين <kbd>&lt;circle&gt;</kbd> صغيرين للعيون<br>4. عنصر <kbd>&lt;line&gt;</kbd> للابتسامة",
"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>&lt;svg&gt;</kbd> (200x200)<br>2. A large <kbd>&lt;circle&gt;</kbd> for the face<br>3. Two small <kbd>&lt;circle&gt;</kbd> elements for eyes<br>4. A <kbd>&lt;line&gt;</kbd> for the smile",
"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=\"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>",
"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>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "svg",
"message": "أضف عنصر <kbd>&lt;svg&gt;</kbd>"
"message": "Add an <kbd>&lt;svg&gt;</kbd> element"
},
{
"type": "element_count",
"value": { "selector": "circle", "min": 3 },
"message": "أضف على الأقل 3 دوائر (1 للوجه + 2 للعيون)"
"message": "Add at least 3 circles (1 face + 2 eyes)"
},
{
"type": "element_exists",
"value": "line",
"message": "أضف <kbd>&lt;line&gt;</kbd> للابتسامة"
"message": "Add a <kbd>&lt;line&gt;</kbd> for the smile"
}
]
}

View File

@@ -2,18 +2,18 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "flexbox",
"title": "CSS Flexbox",
"description": "أتقن نموذج تخطيط الصندوق المرن للتصاميم المتجاوبة الحديثة",
"description": "Master the flexible box layout model for modern responsive designs",
"difficulty": "intermediate",
"lessons": [
{
"id": "flexbox-1",
"title": "Container",
"description": "قبل flexbox، كانت حتى التخطيطات البسيطة تتطلب floats أو حيل التموضع أو تخطيطات الجداول. ثورة Flexbox (تخطيط الصندوق المرن) في CSS من خلال توفير نظام تخطيط أحادي البعد مصمم خصيصاً لتوزيع المساحة ومحاذاة المحتوى.<br><br><strong>كيف يعمل:</strong> عندما تضبط <kbd>display: flex</kbd> على عنصر، يصبح <em>حاوية flex</em>. أبناؤه المباشرون يصبحون تلقائياً <em>عناصر flex</em> تتدفق على طول المحور الرئيسي (أفقياً بشكل افتراضي). هذه الخاصية الواحدة تحول العناصر الكتلية المتراصة إلى صف أفقي.<br><br><strong>المحوران:</strong><br>• <em>المحور الرئيسي</em> الاتجاه الأساسي لتدفق العناصر (row = يسار→يمين)<br>• <em>المحور المتقاطع</em> عمودي على الرئيسي (row = أعلى→أسفل)<br><br><pre>.nav {\n display: flex;\n}</pre>",
"task": "قائمة التنقل هذه تتراص عمودياً. أضف <kbd>display: flex</kbd> لترتيب الروابط أفقياً.",
"previewHTML": "<nav class=\"nav\"><a href=\"#\">Home</a><a href=\"#\">Products</a><a href=\"#\">About</a><a href=\"#\">Contact</a></nav>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .nav { background: #1a1a2e; padding: 1rem; } .nav a { color: white; text-decoration: none; padding: 8px 1rem; border-radius: 4px; } .nav a:hover { background: rgba(255,255,255,0.1); }",
"sandboxCSS": "",
"codePrefix": ".nav {\n ",
"description": "Learn how to create a flex container and understand the main and cross axes.",
"task": "Add <kbd>display: flex</kbd> to <kbd>.wrap</kbd> to create a flexbox layout.",
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; }",
"codePrefix": ".wrap {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "display: flex;",
@@ -21,41 +21,61 @@
"validations": [
{
"type": "property_value",
"value": { "property": "display", "expected": "flex" },
"message": "اضبط <kbd>display: flex</kbd>"
"value": {
"property": "display",
"expected": "flex"
},
"message": "Set <kbd>display: flex</kbd>"
}
]
},
{
"id": "flexbox-2",
"title": "Gap",
"description": "خاصية <kbd>gap</kbd> تضيف تباعداً متسقاً بين عناصر flex بدون الحاجة إلى الهوامش. تُنشئ مساحة فقط بين العناصر، وليس على الحواف.",
"task": "أضف <kbd>gap: 1rem</kbd> لتوزيع روابط التنقل بالتساوي.",
"previewHTML": "<nav class=\"nav\"><a href=\"#\">Home</a><a href=\"#\">Products</a><a href=\"#\">About</a><a href=\"#\">Contact</a></nav>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .nav { background: #1a1a2e; padding: 1rem; display: flex; } .nav a { color: white; text-decoration: none; padding: 8px 1rem; border-radius: 4px; background: rgba(255,255,255,0.1); }",
"sandboxCSS": "",
"codePrefix": ".nav {\n ",
"title": "Direction & Wrap",
"description": "Control the direction and wrapping of flex items within a container.",
"task": "Add <kbd>flex-direction: column</kbd> and <kbd>flex-wrap: wrap</kbd> to <kbd>.wrap</kbd>.",
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div><div class='box'>4</div><div class='box'>5</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; height: 16rem; display: flex; }",
"codePrefix": ".wrap {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "gap: 1rem;",
"solution": "flex-direction: column;\n flex-wrap: wrap;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "gap", "expected": "1rem" },
"message": "اضبط <kbd>gap: 1rem</kbd>"
"value": {
"property": "flex-direction",
"expected": "column"
},
"message": "Set <kbd>flex-direction: column</kbd>",
"options": {
"exact": true
}
},
{
"type": "property_value",
"value": {
"property": "flex-wrap",
"expected": "wrap"
},
"message": "Set <kbd>flex-wrap: wrap</kbd>",
"options": {
"exact": true
}
}
]
},
{
"id": "flexbox-3",
"title": "Justify Content",
"description": "<kbd>justify-content</kbd> يوزع العناصر على طول المحور الرئيسي. القيم الشائعة:<br>• <kbd>flex-start</kbd> تجميع في البداية<br>• <kbd>flex-end</kbd> تجميع في النهاية<br>• <kbd>center</kbd> توسيط العناصر<br>• <kbd>space-between</kbd> مسافة متساوية بين العناصر<br>• <kbd>space-around</kbd> مسافة متساوية حول العناصر",
"task": "ادفع زر \"Login\" إلى اليمين بضبط <kbd>justify-content: space-between</kbd> على التنقل.",
"previewHTML": "<nav class=\"nav\"><div class=\"links\"><a href=\"#\">Home</a><a href=\"#\">Products</a><a href=\"#\">About</a></div><a href=\"#\" class=\"login\">Login</a></nav>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .nav { background: #1a1a2e; padding: 1rem; display: flex; } .links { display: flex; gap: 8px; } .nav a { color: white; text-decoration: none; padding: 8px 1rem; border-radius: 4px; } .nav a:hover { background: rgba(255,255,255,0.1); } .login { background: steelblue; }",
"sandboxCSS": "",
"codePrefix": ".nav {\n ",
"description": "Learn how to align flex items along the main axis of the flex container.",
"task": "Add <kbd>justify-content: space-between</kbd> to <kbd>.wrap</kbd> to distribute the boxes evenly.",
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; }",
"codePrefix": ".wrap {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "justify-content: space-between;",
@@ -63,20 +83,26 @@
"validations": [
{
"type": "property_value",
"value": { "property": "justify-content", "expected": "space-between" },
"message": "اضبط <kbd>justify-content: space-between</kbd>"
"value": {
"property": "justify-content",
"expected": "space-between"
},
"message": "Set <kbd>justify-content: space-between</kbd>",
"options": {
"exact": true
}
}
]
},
{
"id": "flexbox-4",
"title": "Align Items",
"description": "<kbd>align-items</kbd> يتحكم في المحاذاة على المحور المتقاطع (عمودياً عندما يكون flex-direction هو row). القيم تشمل:<br>• <kbd>stretch</kbd> تمديد للملء (افتراضي)<br>• <kbd>flex-start</kbd> محاذاة للأعلى<br>• <kbd>flex-end</kbd> محاذاة للأسفل<br>• <kbd>center</kbd> توسيط عمودي",
"task": "الشعار وروابط التنقل لها ارتفاعات مختلفة. وسّطها عمودياً باستخدام <kbd>align-items: center</kbd>.",
"previewHTML": "<header class=\"header\"><div class=\"logo\">ACME</div><nav><a href=\"#\">Products</a><a href=\"#\">Pricing</a><a href=\"#\">Docs</a></nav></header>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .header { background: white; padding: 1rem 2rem; display: flex; justify-content: space-between; border-bottom: 1px solid #eee; } .logo { font-size: 1.5rem; font-weight: bold; color: steelblue; } nav { display: flex; gap: 1rem; } nav a { color: #333; text-decoration: none; font-size: 0.9rem; }",
"sandboxCSS": "",
"codePrefix": ".header {\n ",
"description": "Control how flex items are aligned along the cross axis of the flex container.",
"task": "Add <kbd>align-items: center</kbd> to <kbd>.wrap</kbd> to vertically center the boxes.",
"previewHTML": "<div class='wrap'><div class='box tall'>1</div><div class='box'>2</div><div class='box short'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; display: flex; justify-content: center; } .tall { height: 6rem; } .short { height: 3rem; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; height: 10rem; }",
"codePrefix": ".wrap {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "align-items: center;",
@@ -84,50 +110,62 @@
"validations": [
{
"type": "property_value",
"value": { "property": "align-items", "expected": "center" },
"message": "اضبط <kbd>align-items: center</kbd>"
"value": {
"property": "align-items",
"expected": "center"
},
"message": "Set <kbd>align-items: center</kbd>",
"options": {
"exact": true
}
}
]
},
{
"id": "flexbox-5",
"title": "Flex Wrap",
"description": "بشكل افتراضي، تنضغط عناصر flex في سطر واحد. <kbd>flex-wrap: wrap</kbd> يسمح للعناصر بالتدفق إلى أسطر متعددة عندما تنفد المساحة.",
"task": "هذه البطاقات تتجاوز الحاوية. أضف <kbd>flex-wrap: wrap</kbd> للسماح لها بالانتقال إلى صفوف جديدة.",
"previewHTML": "<div class=\"cards\"><article class=\"card\">Card 1</article><article class=\"card\">Card 2</article><article class=\"card\">Card 3</article><article class=\"card\">Card 4</article><article class=\"card\">Card 5</article><article class=\"card\">Card 6</article></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .cards { display: flex; gap: 1rem; } .card { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); min-width: 120px; text-align: center; }",
"sandboxCSS": "",
"codePrefix": ".cards {\n ",
"title": "Flex Grow",
"description": "The <kbd>flex</kbd> property controls how much an item grows relative to others.",
"task": "Add <kbd>flex: 2</kbd> to <kbd>.box2</kbd> to make it grow twice as wide.",
"previewHTML": "<div class='wrap'><div class='box box1'>1</div><div class='box box2'>2</div><div class='box box3'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; display: flex; align-items: center; justify-content: center; } .box1 { background: coral; flex: 1; } .box2 { background: mediumseagreen; } .box3 { background: gold; flex: 1; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; }",
"codePrefix": ".box2 {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "flex-wrap: wrap;",
"solution": "flex: 2;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "flex-wrap", "expected": "wrap" },
"message": "اضبط <kbd>flex-wrap: wrap</kbd>"
"value": {
"property": "flex",
"expected": "2"
},
"message": "Set <kbd>flex: 2</kbd>"
}
]
},
{
"id": "flexbox-6",
"title": "Flex Grow",
"description": "خاصية <kbd>flex</kbd> على العناصر تتحكم في كيفية نموها وانكماشها. <kbd>flex: 1</kbd> يجعل العنصر ينمو لملء المساحة المتاحة. عناصر متعددة مع <kbd>flex: 1</kbd> تتشارك المساحة بالتساوي.",
"task": "اجعل حقل البحث يتوسع لملء المساحة المتاحة بضبط <kbd>flex: 1</kbd> على <kbd>.search</kbd>.",
"previewHTML": "<div class=\"toolbar\"><input class=\"search\" type=\"text\" placeholder=\"Search...\"><button class=\"btn\">Search</button><button class=\"btn\">Filters</button></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .toolbar { display: flex; gap: 8px; padding: 1rem; background: #f5f5f5; border-radius: 8px; } .search { padding: 8px 1rem; border: 1px solid #ddd; border-radius: 4px; font-size: 1rem; } .btn { padding: 8px 1rem; background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; }",
"sandboxCSS": "",
"codePrefix": ".search {\n ",
"title": "Align Self",
"description": "Use <kbd>align-self</kbd> to override alignment for a single flex item.",
"task": "Add <kbd>align-self: flex-start</kbd> to <kbd>.middle</kbd> to move it to the top.",
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box middle'>2</div><div class='box'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; } .middle { background: mediumseagreen; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; height: 12rem; align-items: center; }",
"codePrefix": ".middle {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "flex: 1;",
"solution": "align-self: flex-start;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "flex", "expected": "1" },
"message": "اضبط <kbd>flex: 1</kbd>"
"value": {
"property": "align-self",
"expected": "flex-start"
},
"message": "Set <kbd>align-self: flex-start</kbd>"
}
]
}

View File

@@ -1,257 +1,548 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "css-basic-selectors",
"title": "CSS Grundlagen",
"description": "Lerne die grundlegenden Bausteine von CSS: Eigenschaften, Werte und Selektoren. Dieses Modul vermittelt dir die Syntaxregeln, denen jede CSS-Deklaration folgt.",
"title": "CSS Selektoren",
"description": "CSS-Selektoren sind die Grundlage für das Stylen von Webseiten und ermöglichen es dir, bestimmte HTML-Elemente für die Gestaltung auszuwählen. Dieses Modul stellt grundlegende Selektortypen vor, einschließlich Element-Typ-Selektoren, Klassen-Selektoren, ID-Selektoren und des universellen Selektors.",
"difficulty": "beginner",
"lessons": [
{
"id": "css-properties",
"title": "CSS-Eigenschaften",
"description": "CSS gestaltet Elemente mit <strong>Deklarationen</strong> - Paaren aus Eigenschaften und Werten. Jede Deklaration folgt dem gleichen Muster:<br><br><pre>property: value;</pre><br>Die <strong>Eigenschaft</strong> ist das, was du ändern möchtest (wie <kbd>color</kbd> oder <kbd>background</kbd>). Der <strong>Wert</strong> ist das, worauf du es setzt. Ein Doppelpunkt trennt sie, und ein Semikolon beendet die Zeile.<br><br>Werte gibt es in verschiedenen Typen:<br>• <strong>Schlüsselwörter:</strong> <kbd>red</kbd>, <kbd>bold</kbd>, <kbd>center</kbd><br>• <strong>Zahlen mit Einheiten:</strong> <kbd>16px</kbd>, <kbd>2rem</kbd>, <kbd>100%</kbd><br>• <strong>Farben:</strong> <kbd>steelblue</kbd>, <kbd>#ff0000</kbd>",
"task": "Vervollständige die Deklaration, indem du <kbd>color: coral;</kbd> hinzufügst, um die Textfarbe zu ändern.",
"previewHTML": "<p class=\"text\">This text should turn coral.</p>",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } .text { font-size: 1.25rem; }",
"sandboxCSS": "",
"codePrefix": ".text {\n ",
"id": "introduction-to-selectors",
"title": "Was ist ein CSS-Selektor?",
"description": "Ein CSS-Selektor ist der erste Teil einer CSS-Regel, der dem Browser mitteilt, welche HTML-Elemente die im Deklarationsblock definierten Stile erhalten sollen. Selektoren sind im Wesentlichen Muster, die mit Elementen in deinem HTML-Dokument übereinstimmen. Das Verstehen von Selektoren ist grundlegend, da sie bestimmen, welche Elemente von deinen CSS-Regeln betroffen sind. Das Element oder die Elemente, die von einem Selektor anvisiert werden, werden als 'Subjekt des Selektors' bezeichnet. Beim Schreiben einer CSS-Regel gibst du zuerst den Selektor an, gefolgt von geschweiften Klammern, die die Stil-Deklarationen enthalten.<br/>Um beispielsweise die Textfarbe von Elementen zu ändern, kannst du die <kbd>color</kbd>-Eigenschaft in deinem Deklarationsblock verwenden.<br><br><pre>/* Element-Selektor */\np {\n color: orangered;\n /* │ └─── Gibt den Wert des Ausdrucks an\n │ \n └─────────── Gibt die Eigenschaft des Ausdrucks an */\n}</pre>",
"task": "Schreibe eine CSS-Regel mit einem Typ-Selektor, die alle Absatz-Elemente <kbd>p</kbd> im Dokument anvisiert. Mache den Text blau, indem du die <kbd>color</kbd>-Eigenschaft auf <kbd>blue</kbd> setzt.",
"previewHTML": "<h1>Einführung in CSS-Selektoren</h1>\n<p>Dieser Absatz sollte blau werden.</p>\n<div>Dieses div-Element sollte unverändert bleiben.</div>\n<p>Dieser zweite Absatz sollte ebenfalls blau werden.</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h1, p, div { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
"codePrefix": "/* Schreibe einen Typ-Selektor, um alle Absatz-Elemente anzuvisieren */\n",
"initialCode": "",
"codeSuffix": "\n}",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "color: coral;",
"solution": "p { color: blue }",
"validations": [
{
"type": "property_value",
"value": { "property": "color", "expected": "coral" },
"message": "Füge <kbd>color: coral;</kbd> hinzu"
}
]
},
{
"id": "multiple-properties",
"title": "Mehrere Eigenschaften",
"description": "Eine Regel kann mehrere Deklarationen enthalten. Jede steht in einer eigenen Zeile und jede benötigt ein Semikolon am Ende:<br><br><pre>.box {<br> background: gold;<br> color: navy;<br> padding: 1rem;<br>}</pre><br>Die Reihenfolge spielt normalerweise keine Rolle - CSS wendet alle an. Bei Konflikten gewinnt die letzte.",
"task": "Füge zwei Deklarationen hinzu: <kbd>background: lavender;</kbd> und <kbd>padding: 1rem;</kbd>",
"previewHTML": "<div class=\"card\">A styled card with background and padding.</div>",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } .card { border-radius: 8px; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"previewContainer": "preview-area",
"solution": "background: lavender;\n padding: 1rem;",
"validations": [
"type": "regex",
"value": "^p\\s*{",
"message": "Beginne deine Regel mit <kbd>p { … }</kbd>, um alle Absatz-Elemente auszuwählen",
"options": {
"caseSensitive": false
}
},
{
"type": "property_value",
"value": { "property": "background", "expected": "lavender" },
"message": "Füge <kbd>background: lavender;</kbd> hinzu"
"type": "contains",
"value": "color:",
"message": "Füge die <kbd>color:</kbd>-Eigenschaft in deine CSS-Regel ein"
},
{
"type": "contains",
"value": "blue",
"message": "Setze den Farbwert auf <kbd>blue</kbd>"
},
{
"type": "property_value",
"value": { "property": "padding", "expected": "1rem" },
"message": "Füge <kbd>padding: 1rem;</kbd> hinzu"
"value": {
"property": "color",
"expected": "blue"
},
"message": "Verwende <kbd>color: blue</kbd>, um die Textfarbe zu setzen"
},
{
"type": "regex",
"value": "p\\s*{[^}]*}",
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
"options": {
"caseSensitive": false
}
}
]
},
{
"id": "type-selectors",
"title": "Typ-Selektoren",
"description": "Ein <strong>Selektor</strong> sagt dem Browser, welche Elemente gestylt werden sollen. Der einfachste Selektor ist ein <strong>Typ-Selektor</strong> — einfach der HTML-Tag-Name.<br><br><pre>p {<br> color: steelblue;<br>}</pre><br>Diese Regel zielt auf jedes <kbd>&lt;p&gt;</kbd>-Element auf der Seite. Typ-Selektoren eignen sich hervorragend für Basis-Styles.",
"task": "Style alle Absätze. Schreibe eine Regel mit <kbd>p</kbd> als Selektor und setze <kbd>color: steelblue</kbd>.",
"previewHTML": "<article>\n <h2>Fresh Roasted Coffee</h2>\n <p>Our beans are sourced from small farms in Colombia and Ethiopia.</p>\n <p>Each batch is roasted weekly to ensure peak freshness.</p>\n</article>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } h2 { margin: 0 0 1rem; }",
"sandboxCSS": "",
"codePrefix": "",
"initialCode": "",
"title": "Typ-Selektoren: HTML-Elemente anvisieren",
"description": "Typ-Selektoren (auch Tag-Name-Selektoren oder Element-Selektoren genannt) visieren HTML-Elemente basierend auf ihrem Tag-Namen an. Zum Beispiel wählt <kbd>p</kbd> alle Absatz-Elemente, <kbd>h1</kbd> alle Überschriften der ersten Ebene und <kbd>div</kbd> alle Division-Elemente aus. Typ-Selektoren sind die grundlegendste Art, Elemente auszuwählen, und wenden Stile einheitlich auf alle Instanzen eines bestimmten HTML-Elements in deinem Dokument an. Du kannst verschiedene CSS-Eigenschaften mit Typ-Selektoren definieren, wie <kbd>color</kbd> für Textfarbe, <kbd>background-color</kbd> für den Hintergrund und <kbd>font-weight</kbd> für Textbetonung. Sie bieten einen breiten Ansatz für das Styling deiner Seite und sind oft der Ausgangspunkt für spezifischeres Styling mit anderen Selektortypen.",
"task": "Schreibe drei separate CSS-Regeln mit Typ-Selektoren, um bestimmte HTML-Elemente anzuvisieren: mache <kbd>h2</kbd>-Überschriften <kbd>purple</kbd>, gib <kbd>span</kbd>-Elementen einen <kbd>yellow</kbd>-Hintergrund und mache <kbd>strong</kbd>-Elemente <kbd>red</kbd>.",
"previewHTML": "<h2>Typ-Selektoren Beispiel</h2>\n<p>Normaler Absatztext <span>mit einem hervorgehobenen Span</span>, der einen gelben Hintergrund haben sollte.</p>\n<p>Ein weiterer Absatz mit <strong>wichtigem, fetten Text</strong>, der rot sein sollte.</p>\n<h2>Eine weitere Überschrift</h2>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h2, p, span, strong { padding: 3px; }",
"codePrefix": "/* Schreibe drei separate Typ-Selektoren unten */\n\n",
"initialCode": "/* 1. Mache h2-Überschriften lila */\n\n\n/* 2. Gib span-Elementen einen gelben Hintergrund */\n\n\n/* 3. Mache strong-Elemente rot */\n",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "p {\n color: steelblue;\n}",
"solution": "/* 1. Mache h2-Überschriften lila */\nh2 {\n color: purple;\n}\n\n/* 2. Gib span-Elementen einen gelben Hintergrund */\nspan {\n background-color: yellow;\n}\n\n/* 3. Mache strong-Elemente rot */\nstrong {\n color: red;\n}",
"validations": [
{
"type": "regex",
"value": "p\\s*\\{",
"message": "Beginne mit <kbd>p {</kbd>, um Absätze auszuwählen"
"value": "^h2\\s*{",
"message": "Füge einen <kbd>h2 { … }</kbd>-Selektor hinzu"
},
{
"type": "property_value",
"value": { "property": "color", "expected": "steelblue" },
"message": "Setze <kbd>color: steelblue</kbd>"
}
]
},
{
"id": "styling-links",
"title": "Links stylen",
"description": "Typ-Selektoren funktionieren für jedes HTML-Element. Der <kbd>a</kbd>-Selektor zielt auf alle Links einer Seite.<br><br>Links haben standardmäßig eine blaue Farbe und Unterstreichung. Du kannst beides mit CSS ändern — verwende <kbd>color</kbd> für den Text und <kbd>text-decoration: none</kbd>, um die Unterstreichung zu entfernen.",
"task": "Style die Navigationslinks. Schreibe eine Regel mit <kbd>a</kbd> als Selektor und setze <kbd>color: coral</kbd>.",
"previewHTML": "<nav>\n <a href=\"#\">Home</a>\n <a href=\"#\">Menu</a>\n <a href=\"#\">About</a>\n <a href=\"#\">Contact</a>\n</nav>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } nav { display: flex; gap: 1.5rem; }",
"sandboxCSS": "",
"codePrefix": "",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "a {\n color: coral;\n}",
"validations": [
"value": {
"property": "color",
"expected": "purple"
},
"message": "Setze die <kbd>color</kbd>-Eigenschaft auf <kbd>purple</kbd> für h2-Elemente"
},
{
"type": "regex",
"value": "a\\s*\\{",
"message": "Beginne mit <kbd>a {</kbd>, um Links auszuwählen"
"value": "h2\\s*{[^}]*}",
"message": "Vergiss nicht, deine h2-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden"
},
{
"type": "regex",
"value": "^span\\s*{",
"message": "Füge einen <kbd>span { … }</kbd>-Selektor hinzu"
},
{
"type": "property_value",
"value": { "property": "color", "expected": "coral" },
"message": "Setze <kbd>color: coral</kbd>"
"value": {
"property": "background-color",
"expected": "yellow"
},
"message": "Setze <kbd>background-color: yellow</kbd> für span-Elemente"
},
{
"type": "regex",
"value": "span\\s*{[^}]*}",
"message": "Vergiss nicht, deine span-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden"
},
{
"type": "regex",
"value": "^strong\\s*{",
"message": "Füge einen <kbd>strong { … }</kbd>-Selektor hinzu"
},
{
"type": "regex",
"value": "strong\\s*{\\s*color:\\s*red;[^}]*}",
"message": "Setze <kbd>color: red</kbd> für strong-Elemente"
}
]
},
{
"id": "class-selectors",
"title": "Klassen-Selektoren",
"description": "Typ-Selektoren stylen <em>alle</em> Elemente dieses Typs. Aber was, wenn du nur einige davon stylen möchtest?<br><br><strong>Klassen-Selektoren</strong> zielen auf Elemente mit einem bestimmten <kbd>class</kbd>-Attribut. Sie beginnen mit einem Punkt:<br><br><pre>.badge {<br> background: coral;<br>}</pre><br>Das stylt nur Elemente mit <kbd>class=\"badge\"</kbd>.",
"task": "Style das Benachrichtigungs-Badge. Schreibe eine Regel mit <kbd>.badge</kbd> als Selektor und setze <kbd>background: tomato</kbd>.",
"previewHTML": "<header>\n <h1>Dashboard</h1>\n <span class=\"badge\">3</span>\n</header>\n<p>You have new notifications waiting.</p>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem; } h1 { margin: 0; font-size: 1.5rem; } .badge { color: white; padding: 0.25rem 0.5rem; border-radius: 999px; font-size: 0.875rem; } p { color: #555; margin: 0; }",
"sandboxCSS": "",
"codePrefix": "",
"title": "Klassen-Selektoren: Elementgruppen stylen",
"description": "Klassen-Selektoren visieren Elemente mit einem bestimmten Klassen-Attributwert an. Sie beginnen mit einem Punkt (.) gefolgt vom Klassennamen. Klassen sind mächtig, weil sie es ermöglichen, die gleichen Stile auf mehrere Elemente anzuwenden, unabhängig von ihrem Typ. Ein HTML-Element kann mehrere Klassen haben (durch Leerzeichen im class-Attribut getrennt), und eine Klasse kann auf beliebig viele Elemente angewendet werden. Bei der Verwendung von Klassen-Selektoren kannst du Eigenschaften wie <kbd>background-color</kbd> zum Setzen der Hintergrundfarbe und <kbd>font-weight</kbd> zur Kontrolle der Textdicke anwenden. Diese Flexibilität macht Klassen-Selektoren zu einer der am häufigsten verwendeten Methoden zur Anwendung von Stilen in CSS und ermöglicht modulares und wiederverwendbares Styling auf deiner gesamten Website.",
"task": "Erstelle eine CSS-Regel mit einem Klassen-Selektor, die Elemente mit der Klasse <kbd>highlight</kbd> anvisiert. Gib diesen Elementen einen <kbd>yellow</kbd>-Hintergrund und <kbd>bold</kbd>-Text.",
"previewHTML": "<h2>Verwendung von Klassen-Selektoren</h2>\n<p>Dies ist ein normaler Absatz, aber <span class=\"highlight\">dieser Span hat die highlight-Klasse</span> angewendet.</p>\n<p class=\"highlight\">Dieser gesamte Absatz hat die highlight-Klasse.</p>\n<ul>\n <li>Normales Listenelement</li>\n <li class=\"highlight\">Dieses Listenelement ist hervorgehoben</li>\n</ul>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h2, p, li { padding: 5px; margin-bottom: 10px; }",
"codePrefix": "/* Erstelle einen Klassen-Selektor für Elemente mit der 'highlight'-Klasse */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": ".badge {\n background: tomato;\n}",
"validations": [
{
"type": "regex",
"value": "\\.badge\\s*\\{",
"message": "Beginne mit <kbd>.badge {</kbd> (vergiss den Punkt nicht!)"
"value": "^\\.highlight\\s*{",
"message": "Beginne deine Regel mit <kbd>.highlight { … }</kbd>, um einen Klassen-Selektor zu erstellen",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "background-color:",
"message": "Füge die <kbd>background-color:</kbd>-Eigenschaft hinzu"
},
{
"type": "property_value",
"value": { "property": "background", "expected": "tomato" },
"message": "Setze <kbd>background: tomato</kbd>"
"value": {
"property": "background-color",
"expected": "yellow"
},
"message": "Setze die Hintergrundfarbe auf <kbd>yellow</kbd>"
},
{
"type": "contains",
"value": "font-weight:",
"message": "Füge die <kbd>font-weight:</kbd>-Eigenschaft hinzu"
},
{
"type": "property_value",
"value": {
"property": "font-weight",
"expected": "bold"
},
"message": "Setze font-weight auf <kbd>bold</kbd>"
},
{
"type": "regex",
"value": "\\.highlight\\s*{[^}]*}",
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "button-variants",
"title": "Button-Varianten",
"description": "Elemente können mehrere Klassen haben. Wenn du Klassen-Selektoren ohne Leerzeichen verkettst, zielst du auf Elemente, die <em>alle</em> diese Klassen haben:<br><br><pre>.btn.primary {<br> background: steelblue;<br>}</pre><br>Das zielt auf Elemente mit <kbd>class=\"btn primary\"</kbd>, nicht nur auf <kbd>.btn</kbd> oder nur <kbd>.primary</kbd>.",
"task": "Style den primären Button. Schreibe eine Regel mit <kbd>.btn.primary</kbd> als Selektor und setze <kbd>background: steelblue</kbd>.",
"previewHTML": "<div class=\"actions\">\n <button class=\"btn\">Cancel</button>\n <button class=\"btn primary\">Save Changes</button>\n</div>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } .actions { display: flex; gap: 0.75rem; } .btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 1rem; cursor: pointer; background: #e0e0e0; color: #333; }",
"id": "multiple-classes",
"title": "Mit mehreren Klassen arbeiten",
"description": "HTML-Elemente können mehrere Klassen gleichzeitig haben, was komponierbare und modulare CSS-Designs ermöglicht. Wenn ein Element mehrere Klassen hat, erhält es Stile von allen passenden Klassen-Selektoren. Dieser Ansatz ermöglicht es dir, eine Bibliothek wiederverwendbarer CSS-Klassen aufzubauen, die auf verschiedene Arten kombiniert werden können. Du kannst auch Elemente anvisieren, die eine bestimmte Kombination von Klassen haben, indem du Klassen-Selektoren ohne Leerzeichen verkettst (z.B. <kbd>.class1.class2</kbd>). Beim Stylen dieser Elemente könntest du Eigenschaften wie <kbd>border-color</kbd> verwenden, um die Farbe von Element-Rahmen zu ändern, und <kbd>background-color</kbd>, um die Hintergrundfarbe von Elementen zu setzen. Diese Technik ermöglicht bedingte Stile, die nur gelten, wenn bestimmte Klassen zusammen erscheinen.",
"task": "Vervollständige die CSS-Regel, die Elemente mit sowohl <kbd>card</kbd> als auch <kbd>featured</kbd>-Klassen anvisiert, indem du die Selektoren verkettest. Setze border-color auf gold und background-color auf lemonchiffon, um hervorgehobene Karten hervorzuheben.",
"previewHTML": "<h2>Mehrfach-Klassen-Kombinationen</h2>\n<div class=\"card\">Normale Karte</div>\n<div class=\"card featured\">Hervorgehobene Karte</div>\n<div class=\"featured\">Nur hervorgehoben (keine Karte)</div>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .card { border: 2px solid gray; padding: 15px; margin-bottom: 10px; border-radius: 5px; }",
"sandboxCSS": "",
"codePrefix": "",
"codePrefix": "/* Die .card-Klasse hat bereits grundlegendes Styling */\n/* Visiere jetzt Elemente mit BEIDEN Klassen an: 'card' UND 'featured' */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": ".btn.primary {\n background: steelblue;\n}",
"solution": ".card.featured { border-color: gold; background-color: lemonchiffon }",
"validations": [
{
"type": "regex",
"value": "\\.btn\\.primary\\s*\\{",
"message": "Verwende <kbd>.btn.primary {</kbd> (kein Leerzeichen zwischen den Klassen)"
"value": "^\\.card\\.featured\\s*{",
"message": "Verkette die Selektoren als <kbd>.card.featured</kbd> (kein Leerzeichen dazwischen)",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "border-color:",
"message": "Füge die <kbd>border-color</kbd>-Eigenschaft hinzu"
},
{
"type": "property_value",
"value": { "property": "background", "expected": "steelblue" },
"message": "Setze <kbd>background: steelblue</kbd>"
"value": {
"property": "border-color",
"expected": "gold"
},
"message": "Setze die Rahmenfarbe auf <kbd>gold</kbd>"
},
{
"type": "regex",
"value": "\\.card\\.featured\\s*{[^}]*;",
"message": "Vergiss nicht, deine CSS-Regel mit einem Semikolon <kbd>;</kbd> zu beenden"
},
{
"type": "contains",
"value": "background-color:",
"message": "Füge die <kbd>background-color</kbd>-Eigenschaft hinzu"
},
{
"type": "property_value",
"value": {
"property": "background-color",
"expected": "lemonchiffon"
},
"message": "Setze die Hintergrundfarbe auf <kbd>lemonchiffon</kbd>"
},
{
"type": "regex",
"value": "\\.card\\.featured\\s*{[^}]*}",
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "specific-elements",
"title": "Spezifische Elemente ansprechen",
"description": "Manchmal soll eine Klasse auf verschiedenen Elementen unterschiedlich aussehen. Kombiniere einen Typ-Selektor mit einem Klassen-Selektor (ohne Leerzeichen), um spezifischer zu sein:<br><br><pre>a.btn {<br> text-decoration: none;<br>}</pre><br>Das stylt nur <kbd>&lt;a&gt;</kbd>-Elemente mit der <kbd>btn</kbd>-Klasse, nicht <kbd>&lt;button&gt;</kbd>-Elemente mit dieser Klasse.",
"task": "Entferne die Unterstreichung von Link-Buttons. Schreibe eine Regel mit <kbd>a.btn</kbd> als Selektor und setze <kbd>text-decoration: none</kbd>.",
"previewHTML": "<div class=\"actions\">\n <button class=\"btn\">Regular Button</button>\n <a href=\"#\" class=\"btn\">Link Button</a>\n</div>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } .actions { display: flex; gap: 0.75rem; align-items: center; } .btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 1rem; cursor: pointer; background: steelblue; color: white; }",
"sandboxCSS": "",
"codePrefix": "",
"id": "class-with-type",
"title": "Typ- und Klassen-Selektoren kombinieren",
"description": "Du kannst Typ-Selektoren mit Klassen-Selektoren kombinieren, um bestimmte HTML-Elemente anzuvisieren, die eine bestimmte Klasse haben. Dies erstellt einen spezifischeren Selektor, der nur passt, wenn beide Bedingungen wahr sind: das Element ist vom angegebenen Typ UND es hat die angegebene Klasse. Zum Beispiel würde <kbd>p.note</kbd> Absatz-Elemente mit der Klasse <kbd>note</kbd> auswählen, aber keine divs oder spans mit derselben Klasse. Du kannst diese kombinierten Selektionen mit Eigenschaften wie <kbd>background-color</kbd> stylen, um einen farbigen Hintergrund für deine Elemente zu setzen. Dieser Ansatz ermöglicht es dir, verschiedene Stile auf dieselbe Klasse anzuwenden, wenn sie auf verschiedenen Elementtypen erscheint.",
"task": "Erstelle eine CSS-Regel, die speziell <kbd>&lt;span&gt;</kbd>-Elemente mit der Klasse <kbd>highlight</kbd> anvisiert. Gib diesen Elementen einen orangen Hintergrund, während andere Elemente mit der highlight-Klasse unverändert bleiben.",
"previewHTML": "<h2>Typ- und Klassen-Kombinationen</h2>\n<p>Dieser Absatz hat einen <span class=\"highlight\">hervorgehobenen Span</span>, der einen orangen Hintergrund haben sollte.</p>\n<p class=\"highlight\">Dieser Absatz hat die highlight-Klasse, sollte aber KEINEN orangen Hintergrund haben.</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .highlight { font-weight: bold; }",
"sandboxCSS": "h2, p, span { padding: 5px; }",
"codePrefix": "/* Die .highlight-Klasse setzt bereits font-weight auf bold */\n/* Visiere jetzt NUR span-Elemente mit der highlight-Klasse an */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "a.btn {\n text-decoration: none;\n}",
"validations": [
{
"type": "regex",
"value": "a\\.btn\\s*\\{",
"message": "Verwende <kbd>a.btn {</kbd> (Typ + Klasse, kein Leerzeichen)"
"value": "^span\\.highlight\\s*{",
"message": "Verwende den <kbd>span.highlight</kbd>-Selektor (kein Leerzeichen zwischen Element und Klasse)",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "background-color:",
"message": "Füge die <kbd>background-color</kbd>-Eigenschaft hinzu"
},
{
"type": "property_value",
"value": { "property": "text-decoration", "expected": "none" },
"message": "Setze <kbd>text-decoration: none</kbd>"
"value": {
"property": "background-color",
"expected": "orange"
},
"message": "Setze die Hintergrundfarbe auf <kbd>orange</kbd>"
},
{
"type": "regex",
"value": "span\\.highlight\\s*{[^}]*}",
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "grouping-selectors",
"title": "Selektoren gruppieren",
"description": "Wenn mehrere Elemente die gleichen Styles brauchen, liste sie durch Kommas getrennt auf. Das hält dein CSS sauber und wartbar.<br><br><pre>h1, h2, h3 {<br> color: steelblue;<br>}</pre><br>Das wendet die gleiche Farbe auf alle drei Überschriftenebenen in einer Regel an.",
"task": "Style alle Überschriften einheitlich. Füge <kbd>color: steelblue</kbd> zum gruppierten <kbd>h1, h2, h3</kbd>-Selektor hinzu.",
"previewHTML": "<article><h1>Main Title</h1><p>Introduction paragraph with some text.</p><h2>Section Heading</h2><p>More content here.</p><h3>Subsection</h3><p>Final paragraph.</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } article { max-width: 500px; } p { color: #555; line-height: 1.6; }",
"sandboxCSS": "",
"codePrefix": "h1, h2, h3 {\n ",
"id": "id-selectors",
"title": "ID-Selektoren: Einzigartige Elemente anvisieren",
"description": "ID-Selektoren visieren Elemente mit einem bestimmten id-Attribut an. Sie beginnen mit einem Raute-/Hash-Zeichen (#) gefolgt vom ID-Namen. Im Gegensatz zu Klassen müssen IDs innerhalb eines Dokuments einzigartig sein jeder ID-Wert sollte nur einmal pro Seite verwendet werden. ID-Selektoren haben eine höhere Spezifität als Klassen- oder Element-Selektoren, was bedeutet, dass sie diese Selektoren bei Konflikten überschreiben. Beim Stylen mit ID-Selektoren kannst du Eigenschaften wie <kbd>color</kbd> verwenden, um die Textfarbe zu definieren, und <kbd>text-decoration</kbd>, um das Erscheinungsbild von Text zu kontrollieren, wie das Hinzufügen von Unterstreichungen zu Elementen. Wegen ihrer Einzigartigkeitsanforderung werden IDs am besten für einmalige Elemente wie Seitenköpfe, Hauptnavigation oder spezifische einzigartige Komponenten verwendet, die nur einmal auf einer Seite erscheinen.",
"task": "Erstelle eine CSS-Regel mit einem ID-Selektor, die das Element mit der ID <kbd>main-title</kbd> anvisiert. Setze seine Farbe auf purple und füge eine Unterstreichung mit <kbd>text-decoration: underline</kbd> hinzu.",
"previewHTML": "<h1 id=\"main-title\">Haupt-Seitentitel</h1>\n<p>Normaler Absatzinhalt.</p>\n<h2>Sekundäre Überschrift</h2>\n<p id=\"intro\">Einführungsabsatz (andere ID).</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h1, h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
"codePrefix": "/* Erstelle einen ID-Selektor für das Element mit id=\"main-title\" */\n",
"initialCode": "",
"codeSuffix": "\n}",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "color: steelblue;",
"validations": [
{
"type": "regex",
"value": "^#main-title\\s*{",
"message": "Beginne deine Regel mit <kbd>#main-title</kbd>, um einen ID-Selektor zu erstellen",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "color:",
"message": "Füge die <kbd>color</kbd>-Eigenschaft hinzu"
},
{
"type": "property_value",
"value": { "property": "color", "expected": "steelblue" },
"message": "Setze <kbd>color: steelblue</kbd>"
"value": {
"property": "color",
"expected": "purple"
},
"message": "Setze die Farbe auf <kbd>purple</kbd>"
},
{
"type": "contains",
"value": "text-decoration:",
"message": "Füge die <kbd>text-decoration</kbd>-Eigenschaft hinzu"
},
{
"type": "property_value",
"value": {
"property": "text-decoration",
"expected": "underline"
},
"message": "Setze text-decoration auf <kbd>underline</kbd>"
},
{
"type": "regex",
"value": "#main-title\\s*{[^}]*}",
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "descendant-selectors",
"title": "Nachfahren-Selektoren",
"description": "Ziele auf Elemente innerhalb anderer Elemente mit einem Leerzeichen zwischen den Selektoren. Das ist eines der nützlichsten Muster in CSS.<br><br><pre>.nav a {<br> color: white;<br>}</pre><br>Das stylt nur Links innerhalb von <kbd>.nav</kbd> und lässt andere Links unverändert.",
"task": "Style Navigationslinks anders. Schreibe eine Regel mit <kbd>.nav a</kbd> als Selektor und setze <kbd>color: white</kbd>.",
"previewHTML": "<nav class=\"nav\"><a href=\"#\">Home</a><a href=\"#\">About</a><a href=\"#\">Contact</a></nav><p>Read more in our <a href=\"#\">documentation</a>.</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; margin: 0; } .nav { background: steelblue; padding: 1rem; display: flex; gap: 1rem; border-radius: 8px; margin-bottom: 1rem; } .nav a { text-decoration: none; } p a { color: steelblue; }",
"sandboxCSS": "",
"codePrefix": "",
"id": "id-with-type",
"title": "Typ- und ID-Selektoren kombinieren",
"description": "Ähnlich wie du Typ- und Klassen-Selektoren kombinieren kannst, kannst du auch Typ-Selektoren mit ID-Selektoren kombinieren. Zum Beispiel visiert <kbd>h1#title</kbd> ein h1-Element mit der ID 'title' an. Bei diesem kombinierten Ansatz kannst du CSS-Eigenschaften wie <kbd>font-style</kbd> anwenden, um die Neigung des Textes zu kontrollieren und ihn kursiv oder normal zu machen. Obwohl diese Selektor-Kombination spezifischer ist als die Verwendung nur des ID-Selektors, ist sie oft unnötig, da IDs bereits einzigartig im Dokument sein sollten. Diese Technik kann jedoch nützlich sein, um die Lesbarkeit des Codes zu verbessern oder wenn du betonen möchtest, dass eine bestimmte ID nur auf einem bestimmten Elementtyp erscheinen sollte.",
"task": "Erstelle eine CSS-Regel, die einen Typ-Selektor mit einem ID-Selektor kombiniert, um speziell ein Absatz-Element mit der ID <kbd>special</kbd> anzuvisieren. Setze seinen Schriftstil auf kursiv.",
"previewHTML": "<h2 id=\"special\">Überschrift mit ID \"special\" (sollte NICHT betroffen sein)</h2>\n<p id=\"special\">Absatz mit ID \"special\" (sollte kursiv werden)</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
"codePrefix": "/* Erstelle einen kombinierten Typ+ID-Selektor für einen Absatz mit id=\"special\" */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": ".nav a {\n color: white;\n}",
"validations": [
{
"type": "regex",
"value": "\\.nav\\s+a\\s*\\{",
"message": "Verwende <kbd>.nav a {</kbd> (Leerzeichen zwischen .nav und a)"
"value": "^p#special\\s*{",
"message": "Verwende <kbd>p#special</kbd>, um Absätze mit ID <kbd>special</kbd> anzuvisieren",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "font-style:",
"message": "Füge die <kbd>font-style</kbd>-Eigenschaft hinzu"
},
{
"type": "property_value",
"value": { "property": "color", "expected": "white" },
"message": "Setze <kbd>color: white</kbd>"
"value": {
"property": "font-style",
"expected": "italic"
},
"message": "Setze font-style auf <kbd>italic</kbd>"
},
{
"type": "regex",
"value": "p#special\\s*{[^}]*}",
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "nested-styling",
"title": "Verschachtelte Styles",
"description": "Nachfahren-Selektoren ermöglichen kontextabhängige Styles. Das gleiche Element kann je nach Position unterschiedlich aussehen.<br><br>Zum Beispiel könnten Absätze in einer <kbd>.card</kbd> kleiner sein als Absätze in einem <kbd>article</kbd>.",
"task": "Mache Absätze innerhalb der Karte kleiner. Schreibe eine Regel mit <kbd>.card p</kbd> als Selektor und setze <kbd>font-size: 0.9rem</kbd>.",
"previewHTML": "<article><h2>Article Title</h2><p>This is a regular article paragraph with normal-sized text for comfortable reading.</p><div class=\"card\"><strong>Quick Tip</strong><p>Card paragraphs should be slightly smaller to fit the compact design.</p></div><p>Back to regular article text here.</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } article { max-width: 500px; } h2 { color: steelblue; margin-top: 0; } p { line-height: 1.6; color: #444; } .card { background: #f0f4f8; padding: 1rem; border-radius: 8px; border-left: 4px solid steelblue; } .card strong { color: steelblue; display: block; margin-bottom: 0.5rem; }",
"sandboxCSS": "",
"codePrefix": "",
"id": "selector-lists",
"title": "Selektor-Listen: Gleiche Regeln auf mehrere Selektoren anwenden",
"description": "Wenn mehrere Elemente das gleiche Styling benötigen, kannst du sie mit einer Selektor-Liste (auch Gruppierungs-Selektoren genannt) zusammenfassen. Selektor-Listen werden erstellt, indem einzelne Selektoren durch Kommas getrennt werden. Dieser Ansatz reduziert Wiederholungen in deinem CSS und macht es wartbarer und effizienter. Zum Beispiel wendet <kbd>h1, h2, h3 { color: blue; }</kbd> die gleiche blaue Farbe auf alle drei Überschriftenebenen an. Beim Stylen mehrerer Selektoren gleichzeitig kannst du Eigenschaften wie <kbd>background-color</kbd> für den Hintergrund, <kbd>border-left</kbd> für einen linken Rahmen mit bestimmter Dicke, Stil und Farbe, und <kbd>padding-left</kbd> anwenden, um Abstand zwischen dem Inhalt und dem linken Rahmen zu schaffen. Leerzeichen um Kommas sind optional, und jeder Selektor in der Liste kann ein beliebiger gültiger Selektortyp sein Elemente, Klassen, IDs oder sogar komplexere Selektoren.",
"task": "Erstelle eine Selektor-Liste, die die gleichen Stile auf drei verschiedene Elemente anwendet: Absätze mit der Klasse <kbd>note</kbd>, Listenelemente mit der Klasse <kbd>important</kbd> und das Element mit der ID <kbd>summary</kbd>. Gib ihnen einen <kbd>lightyellow</kbd>-Hintergrund, einen <kbd>gold</kbd>-linken Rahmen und etwas linkes <kbd>padding</kbd>.",
"previewHTML": "<p class=\"note\">Dies ist ein Notiz-Absatz.</p>\n<ul>\n <li>Normales Listenelement</li>\n <li class=\"important\">Wichtiges Listenelement</li>\n</ul>\n<div id=\"summary\">Zusammenfassungs-Abschnitt</div>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "p, li, div { padding: 8px; margin-bottom: 8px; border: 1px dashed gray; }",
"codePrefix": "/* Erstelle eine Selektor-Liste, um die gleichen Stile auf mehrere verschiedene Elemente anzuwenden */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": ".card p {\n font-size: 0.9rem;\n}",
"solution": "p.note,\nli.important,\n#summary {\n background-color: lightyellow;\n border-left: 3px solid gold;\n padding-left: 10px\n}",
"validations": [
{
"type": "contains",
"value": "p.note",
"message": "Füge <kbd>p.note</kbd> in deine Selektor-Liste ein",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "li.important",
"message": "Füge <kbd>li.important</kbd> in deine Selektor-Liste ein",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "#summary",
"message": "Füge <kbd>#summary</kbd> in deine Selektor-Liste ein",
"options": {
"caseSensitive": true
}
},
{
"type": "regex",
"value": "\\.card\\s+p\\s*\\{",
"message": "Verwende <kbd>.card p {</kbd> (Leerzeichen zwischen .card und p)"
"value": "(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)",
"message": "Erstelle eine kommagetrennte Liste mit allen drei Selektoren",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "background-color:",
"message": "Füge die <kbd>background-color</kbd>-Eigenschaft hinzu"
},
{
"type": "property_value",
"value": { "property": "font-size", "expected": "0.9rem" },
"message": "Setze <kbd>font-size: 0.9rem</kbd>"
"value": {
"property": "background-color",
"expected": "lightyellow"
},
"message": "Setze die Hintergrundfarbe auf <kbd>lightyellow</kbd>"
},
{
"type": "contains",
"value": "border-left:",
"message": "Füge die <kbd>border-left</kbd>-Eigenschaft hinzu"
},
{
"type": "property_value",
"value": {
"property": "border-left",
"expected": "3px solid gold"
},
"message": "Verwende <kbd>border-left: 3px solid gold</kbd>, um einen linken Rahmen zu erstellen"
},
{
"type": "contains",
"value": "padding-left:",
"message": "Füge die <kbd>padding-left</kbd>-Eigenschaft hinzu"
},
{
"type": "property_value",
"value": {
"property": "padding-left",
"expected": "10px"
},
"message": "Verwende <kbd>padding-left: 10px</kbd>, um linkes Padding hinzuzufügen"
}
]
},
{
"id": "universal-selector",
"title": "Der universelle Selektor: Alles anvisieren",
"description": "Der universelle Selektor wird durch ein Sternchen (*) gekennzeichnet und passt auf jedes Element jedes Typs. Er wählt alles im Dokument aus oder, wenn er mit anderen Selektoren kombiniert wird, alles innerhalb eines bestimmten Kontexts. Zum Beispiel entfernt <kbd>* { margin: 0; }</kbd> Ränder von allen Elementen, während <kbd>article *</kbd> alle Elemente innerhalb von article-Elementen auswählt. Bei der Verwendung des universellen Selektors in Kombination mit anderen Selektoren kannst du Eigenschaften wie <kbd>margin</kbd> anwenden, um die Abstände um Elemente zu kontrollieren. Der universelle Selektor ist mächtig, sollte aber wegen seiner breiten Auswirkung vorsichtig verwendet werden. Er wird häufig in CSS-Resets verwendet, um Standard-Browser-Styling zu überschreiben, oder um alle Kinder eines bestimmten Elements anzuvisieren.",
"task": "Verwende den universellen Selektor, um Ränder von allen Elementen innerhalb des Container-divs zu entfernen. Erstelle eine Regel mit <kbd>div.container *</kbd> als Selektor und setze <kbd>margin: 0</kbd>.",
"previewHTML": "<div class=\"container\">\n <h2>Innerhalb des Containers</h2>\n <p>Dieser Absatz ist innerhalb des Containers.</p>\n <ul>\n <li>Listenelement innerhalb des Containers</li>\n </ul>\n</div>\n<p>Dieser Absatz ist außerhalb des Containers und sollte nicht betroffen sein.</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } div.container { border: 2px solid navy; padding: 15px; background-color: lavender; } h2, p, ul, li { margin: 15px 0; }",
"sandboxCSS": "",
"codePrefix": "/* Verwende den universellen Selektor, um alle Elemente innerhalb des Containers anzuvisieren */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "^div\\.container\\s+\\*\\s*{",
"message": "Verwende <kbd>div.container *</kbd>-Selektor (mit einem Leerzeichen zwischen container und *)",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "margin:",
"message": "Füge die <kbd>margin</kbd>-Eigenschaft hinzu"
},
{
"type": "property_value",
"value": {
"property": "margin",
"expected": "0"
},
"message": "Setze margin auf <kbd>0</kbd>"
},
{
"type": "regex",
"value": "div\\.container\\s+\\*\\s*{[^}]*}",
"message": "Vergiss nicht, deine CSS-Regel mit einer schließenden Klammer <kbd>}</kbd> zu beenden",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "specificity-basics",
"title": "Selektor-Spezifität verstehen",
"description": "CSS-Spezifität bestimmt, welche Stile Vorrang haben, wenn mehrere widersprüchliche Regeln auf dasselbe Element abzielen. Spezifität folgt einem hierarchischen System: Inline-Stile haben die höchste Spezifität, gefolgt von ID-Selektoren, dann Klassen-/Attribut-/Pseudo-Klassen-Selektoren und schließlich Element-/Pseudo-Element-Selektoren. Dies kann als vierteilige Punktzahl (inline, ID, Klasse, Element) konzeptualisiert werden. Beim Erstellen mehrerer Regeln, die auf dieselben Elemente abzielen könnten, kannst du die <kbd>color</kbd>-Eigenschaft verwenden, um Textfarben zu setzen, und die Spezifität bestimmt, welche Farbe tatsächlich angewendet wird. Das Verstehen von Spezifität ist entscheidend für vorhersagbares Styling und das Debuggen von CSS-Konflikten. Wenn zwei Selektoren gleiche Spezifität haben, gewinnt derjenige, der zuletzt im Stylesheet kommt.",
"task": "Untersuche die vorhandenen CSS-Regeln und füge eine neue Regel mit höherer Spezifität hinzu, um die Textfarbe des Absatzes zu überschreiben. Erstelle eine Regel mit '.content p' als Selektor und setze color: green.",
"previewHTML": "<div class=\"content\">\n <p>Welche Farbe wird dieser Absatz haben? Schau dir die CSS-Regeln und ihre Spezifität an.</p>\n</div>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "p { border: 1px dashed gray; padding: 10px; }",
"codePrefix": "/* Diese CSS-Regeln visieren denselben Absatz an, haben aber unterschiedliche Spezifität */\n\n/* Regel 1: Element-Selektor (niedrigste Spezifität) */\np {\n color: red;\n}\n\n/* Regel 2: Nachfahren-Selektor (höhere Spezifität als nur 'p') */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "^\\.content\\s+p\\s*{",
"message": "Verwende <kbd>.content p</kbd> als deinen Selektor (beachte das Leerzeichen dazwischen)",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "color:",
"message": "Füge die <kbd>color</kbd>-Eigenschaft hinzu"
},
{
"type": "contains",
"value": "green",
"message": ""
}
]
}

View File

@@ -1,17 +1,16 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "welcome",
"title": "Willkommen",
"description": "Erste Schritte mit Code Crispies",
"mode": "css",
"title": "Code Crispies",
"description": "Willkommen bei Code Crispies - deine interaktive Lernplattform für Webentwicklung",
"mode": "html",
"difficulty": "beginner",
"excludeFromProgress": true,
"lessons": [
{
"id": "hello",
"title": "Hallo!",
"description": "<strong>Willkommen bei Code Crispies!</strong> Lerne CSS und HTML durch praktische Übungen.<br><br><strong>So funktioniert's:</strong><br>1. Lies die Aufgabe links<br>2. Schreibe Code im Editor<br>3. Sieh Live-Ergebnisse in der Vorschau<br><br><strong>Tipps:</strong> Nutze <kbd>Strg+Z</kbd> zum Rückgängigmachen. Öffne das Menü (☰) um alle Module zu erkunden.",
"task": "Schreibe <code>Hello World</code> zum Starten",
"id": "get-started",
"title": "Los geht's",
"description": "<strong>Code Crispies</strong> ist eine kostenlose Open-Source-Plattform zum Erlernen von Webentwicklung durch praktische Übungen. Kein Konto erforderlich!<br><br><strong>Was du lernst:</strong><br>• <strong>HTML</strong> - Semantische Elemente, Formulare, Tabellen, SVG (<em>HTML Block & Inline</em>, <em>HTML Formulare</em>, <em>HTML Tabellen</em>)<br>• <strong>CSS</strong> - Selektoren, Box-Model, Flexbox, Animationen (<em>CSS Selektoren</em>, <em>CSS Box-Model</em>, <em>CSS Flexbox</em>)<br>• <strong>Responsive Design</strong> - Media Queries und Mobile-First Layouts<br><br><strong>So funktioniert's:</strong><br>1. Lies die Aufgabe im linken Bereich<br>2. Schreibe Code im Editor<br>3. Sieh Live-Ergebnisse in der Vorschau<br>4. Bekomme sofortiges Feedback mit Hinweisen<br><br><strong>Tastenkürzel:</strong> <kbd>Strg+Z</kbd> rückgängig, <kbd>Strg+Umschalt+Z</kbd> wiederholen<br><br><strong>Weitere Ressourcen:</strong><br>• <a href=\"https://nextlevelshit.github.io/html-over-js/\" target=\"_blank\">HTML over JS</a> - Native HTML vs JavaScript-Lösungen<br>• <a href=\"https://nextlevelshit.github.io/web-engineering-mandala/\" target=\"_blank\">Web Engineering Mandala</a> - JavaScript Technologie-Roadmap",
"task": "Schreibe <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": "",
@@ -25,6 +24,39 @@
"message": "Schreibe <code>Hello World</code>"
}
]
},
{
"id": "overview",
"title": "Übersicht",
"description": "<strong>Du bist bereit!</strong> Öffne das Menü (☰) um alle Module zu erkunden.<br><br><strong>Empfohlener Lernpfad:</strong><br>1. <em>HTML Block & Inline</em> - Container vs Inline-Elemente verstehen<br>2. <em>HTML Formulare</em> - Interaktive Formulare mit Validierung erstellen<br>3. <em>CSS Selektoren</em> - Elemente präzise ansprechen<br>4. <em>CSS Box-Model</em> - Padding, Margin, Borders meistern<br>5. <em>CSS Flexbox</em> - Flexible Layouts erstellen<br>6. <em>CSS Animationen</em> - Bewegung und Übergänge hinzufügen<br><br><strong>Tipps:</strong><br>• Nutze <em>Lösung einblenden</em> um das Zielergebnis zu sehen<br>• Dein Fortschritt wird automatisch gespeichert<br>• Probiere Emmet im HTML-Modus: <kbd>ul>li*3</kbd> + Tab<br><br><strong>Open Source:</strong><br>• <a href=\"https://git.librete.ch/libretech/code-crispies\" target=\"_blank\">Gitea (Source)</a> · <a href=\"https://github.com/nextlevelshit/code-crispies\" target=\"_blank\">GitHub (Mirror)</a><br>• Entwickelt von <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": "Klicke auf Weiter",
"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": "",
"initialCode": "<p>Hello World! 🌍</p>\n<p>Hallo Welt!</p>\n<p>Bonjour le monde!</p>\n<p>¡Hola Mundo!</p>\n<p>Ciao Mondo!</p>\n<p>Olá Mundo!</p>\n<p>こんにちは世界!</p>\n<p>你好世界!</p>\n<p>안녕 세상!</p>\n<p>Привет мир!</p>\n<p dir=\"rtl\">שלום עולם!</p>\n<p dir=\"rtl\">مرحبا بالعالم!</p>",
"solution": "<p>Hello World! 🌍</p>\n<p>Hallo Welt!</p>\n<p>Bonjour le monde!</p>\n<p>¡Hola Mundo!</p>\n<p>Ciao Mondo!</p>\n<p>Olá Mundo!</p>\n<p>こんにちは世界!</p>\n<p>你好世界!</p>\n<p>안녕 세상!</p>\n<p>Привет мир!</p>\n<p dir=\"rtl\">שלום עולם!</p>\n<p dir=\"rtl\">مرحبا بالعالم!</p>",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "Hello",
"message": "Klicke auf Weiter"
}
]
},
{
"id": "playground",
"title": "Playground",
"mode": "playground",
"description": "",
"task": "",
"previewHTML": "",
"previewBaseCSS": "",
"sandboxCSS": "",
"initialCode": "<style>\n body {\n font-family: system-ui, sans-serif;\n padding: 20px;\n }\n</style>\n\n<h1>Hello World</h1>\n<p>Start coding!</p>",
"solution": "",
"previewContainer": "preview-area",
"validations": []
}
]
}

View File

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

View File

@@ -7,94 +7,119 @@
"lessons": [
{
"id": "units-1",
"title": "Relative Units",
"description": "CSS bietet zwei Arten von Einheiten: <em>absolute</em> (wie <kbd>px</kbd>) und <em>relative</em> (wie <kbd>%</kbd> und <kbd>rem</kbd>). Relative Einheiten passen sich ihrem Kontext an und machen Layouts flexibel und zugänglich.<br><br><strong>Häufige relative Einheiten:</strong><br>• <kbd>%</kbd> Relativ zum Elternelement<br>• <kbd>rem</kbd> Relativ zur Root-Schriftgröße (typisch 16px)<br>• <kbd>em</kbd> Relativ zur Schriftgröße des Elements<br><br>Ein häufiges Muster für lesbaren Inhalt: setze <kbd>width: 100%</kbd>, damit es den verfügbaren Platz füllt, dann <kbd>max-width: 40rem</kbd> um die Zeilenlänge für Lesbarkeit zu begrenzen.",
"task": "Dieser Artikeltext läuft auf großen Bildschirmen zu breit. Füge <kbd>max-width: 40rem</kbd> hinzu für optimale Lesebreite.",
"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; }",
"title": "Absolute vs. Relative Einheiten",
"description": "Lerne den Unterschied zwischen px, rem, em, % und vw/vh für flexible, responsive Layouts.",
"task": "Setze die Breite von <kbd>.box</kbd> auf <kbd>80%</kbd> und max-width auf <kbd>37.5rem</kbd>.",
"previewHTML": "<div class=\"box\">Ändere meine Größe!</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { background: #f5f5f5; padding: 1rem; }",
"sandboxCSS": "",
"codePrefix": ".article {\n ",
"codePrefix": "/* Setze flexible Größen */\n.box {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "max-width: 40rem;",
"codeSuffix": "}",
"solution": " width: 80%;\n max-width: 37.5rem;",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "width",
"message": "Verwende die <kbd>width</kbd> Eigenschaft",
"options": { "caseSensitive": false }
},
{ "type": "property_value", "value": { "property": "width", "expected": "80%" }, "message": "Setze width auf <kbd>80%</kbd>" },
{
"type": "contains",
"value": "max-width",
"message": "Verwende die <kbd>max-width</kbd> Eigenschaft",
"options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "max-width", "expected": "40rem" },
"message": "Setze <kbd>max-width: 40rem</kbd>"
"value": { "property": "max-width", "expected": "37.5rem" },
"message": "Setze max-width auf <kbd>37.5rem</kbd>"
}
]
},
{
"id": "units-2",
"title": "CSS Variables",
"description": "CSS Custom Properties (Variablen) erlauben dir, wiederverwendbare Werte zu definieren. Definiere sie mit <kbd>--name</kbd> und verwende sie mit <kbd>var(--name)</kbd>. Variablen auf <kbd>:root</kbd> sind überall verfügbar.",
"task": "Definiere <kbd>--brand: steelblue</kbd> in <kbd>:root</kbd>, dann verwende es als <kbd>background</kbd>-Farbe für <kbd>.btn</kbd>.",
"previewHTML": "<div class=\"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; }",
"title": "CSS Custom Properties",
"description": "Definiere und verwende Variablen (--custom properties) wieder, um deine Theme-Werte zu zentralisieren.",
"task": "Erstelle eine <code>--main-color</code> Variable in :root mit <kbd>#6200ee</kbd> und wende sie als Rahmenfarbe auf <kbd>.themed</kbd> an.",
"previewHTML": "<div class=\"themed\">Variablen-Box</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .themed { padding: 1rem; border: 0.125rem solid #ddd; }",
"sandboxCSS": "",
"codePrefix": ":root {\n ",
"codePrefix": "/* Definiere und verwende eine CSS-Variable */\n:root {",
"initialCode": "",
"codeSuffix": "\n}\n\n.btn {\n background: var(--brand);\n}",
"solution": "--brand: steelblue;",
"codeSuffix": "}\n.themed { }",
"solution": " --main-color: #6200ee;\n}\n.themed {\n border-color: var(--main-color);",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "--brand",
"message": "Definiere die <kbd>--brand</kbd> Variable",
"value": "--main-color",
"message": "Definiere <kbd>--main-color</kbd> in :root",
"options": { "caseSensitive": false }
},
{
"type": "contains",
"value": "steelblue",
"message": "Setze den Wert auf <kbd>steelblue</kbd>",
"value": "var(--main-color)",
"message": "Verwende <kbd>var(--main-color)</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "border", "expected": "var(--main-color)" },
"message": "Wende die Variable auf die Rahmenfarbe an",
"options": { "exact": false }
}
]
},
{
"id": "units-3",
"title": "calc() Function",
"description": "Die <kbd>calc()</kbd> Funktion ermöglicht das Mischen verschiedener Einheiten in Berechnungen. Das ist essenziell für Layouts, die feste und flexible Größen kombinieren, wie ein Sidebar-Layout.",
"task": "Der Hauptinhalt soll den verbleibenden Platz nach der 200px Sidebar füllen. Setze <kbd>width: calc(100% - 200px)</kbd> auf <kbd>.main</kbd>.",
"previewHTML": "<div class=\"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; }",
"title": "Einheiten-Berechnungen (calc)",
"description": "Verwende die <code>calc()</code> Funktion, um verschiedene Einheiten in einem Ausdruck zu kombinieren.",
"task": "Setze die Breite von <kbd>.sized</kbd> auf <kbd>calc(100% - 2rem)</kbd> und min-height auf <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; }",
"sandboxCSS": "",
"codePrefix": ".main {\n ",
"codePrefix": "/* Verwende calc für dynamische Größen */\n.sized {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "width: calc(100% - 200px);",
"codeSuffix": "}",
"solution": " width: calc(100% - 2rem);\n min-height: calc(10vh + 1rem);",
"previewContainer": "preview-area",
"validations": [
{ "type": "contains", "value": "calc", "message": "Verwende die <kbd>calc()</kbd> Funktion", "options": { "caseSensitive": false } },
{
"type": "regex",
"value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
"message": "Setze <kbd>width: calc(100% - 200px)</kbd>",
"value": "width:\\s*calc\\(100% - 2rem\\)",
"message": "Width sollte calc(100% - 2rem) sein",
"options": { "caseSensitive": false }
},
{
"type": "regex",
"value": "min-height:\\s*calc\\(10vh \\+ 1rem\\)",
"message": "Min-height sollte calc(10vh + 1rem) sein",
"options": { "caseSensitive": false }
}
]
},
{
"id": "units-4",
"title": "Viewport Units",
"description": "Viewport-Einheiten dimensionieren Elemente relativ zum Browserfenster:<br>• <kbd>vw</kbd> 1% der Viewport-Breite<br>• <kbd>vh</kbd> 1% der Viewport-Höhe<br><br>Diese sind perfekt für Vollbild-Sektionen wie Hero-Banner.",
"task": "Mache diese Hero-Sektion so hoch wie das Viewport mit <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; }",
"title": "Viewport & Responsive Einheiten",
"description": "Steuere Layouts relativ zur Viewport-Größe mit vw, vh und vmin/vmax Einheiten.",
"task": "Gib <kbd>.view</kbd> eine Breite von <kbd>50vw</kbd> und Höhe von <kbd>20vh</kbd>.",
"previewHTML": "<div class=\"view\">Viewport-Box</div>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .view { background: #ffe0b2; }",
"sandboxCSS": "",
"codePrefix": ".hero {\n ",
"codePrefix": "/* Verwende Viewport-Einheiten */\n.view {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "min-height: 100vh;",
"codeSuffix": "}",
"solution": " width: 50vw;\n height: 20vh;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "min-height", "expected": "100vh" },
"message": "Setze <kbd>min-height: 100vh</kbd>"
}
{ "type": "contains", "value": "vw", "message": "Verwende <kbd>vw</kbd> Einheit", "options": { "caseSensitive": false } },
{ "type": "contains", "value": "vh", "message": "Verwende <kbd>vh</kbd> Einheit", "options": { "caseSensitive": false } },
{ "type": "property_value", "value": { "property": "width", "expected": "50vw" }, "message": "Setze width auf <kbd>50vw</kbd>" },
{ "type": "property_value", "value": { "property": "height", "expected": "20vh" }, "message": "Setze height auf <kbd>20vh</kbd>" }
]
}
]

View File

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

View File

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

View File

@@ -63,6 +63,35 @@
"message": "Füge eine <kbd>&lt;h1&gt;</kbd>-Überschrift in deinem Header hinzu"
}
]
},
{
"id": "div-vs-span",
"title": "Generische Container: div und span",
"description": "Wenn du einen Container ohne semantische Bedeutung benötigst:<br><br><kbd>&lt;div&gt;</kbd> - Generischer Block-Container (für Layout/Gruppierung)<br><kbd>&lt;span&gt;</kbd> - Generischer Inline-Container (zum Stylen von Textteilen)<br><br>Verwende semantische Elemente wenn möglich, div/span wenn kein semantisches Element passt.",
"task": "Umschließe das Wort 'hervorgehoben' mit einem <kbd>&lt;span&gt;</kbd>, um es anders zu gestalten. Umschließe das gesamte Zitat mit einem <kbd>&lt;div&gt;</kbd>.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: Georgia, serif; padding: 20px; } div { background: #f5f5f5; padding: 15px; border-left: 4px solid #1976d2; } span { background: #fff59d; padding: 2px 4px; }",
"sandboxCSS": "",
"initialCode": "Der hervorgehoben Moment war unvergesslich.",
"solution": "<div>Der <span>hervorgehoben</span> Moment war unvergesslich.</div>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "div",
"message": "Umschließe alles mit einem <kbd>&lt;div&gt;</kbd>-Element"
},
{
"type": "element_exists",
"value": "span",
"message": "Füge ein <kbd>&lt;span&gt;</kbd> um das Wort 'hervorgehoben' hinzu"
},
{
"type": "element_text",
"value": { "selector": "span", "text": "hervorgehoben" },
"message": "Das <kbd>&lt;span&gt;</kbd> sollte das Wort 'hervorgehoben' enthalten"
}
]
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,13 +7,13 @@
"lessons": [
{
"id": "flexbox-1",
"title": "Container",
"description": "Vor Flexbox erforderten selbst einfache Layouts Floats, Positionierungs-Hacks oder tabellenbasierte Layouts. Flexbox (Flexible Box Layout) revolutionierte CSS, indem es ein eindimensionales Layout-System speziell für Platzverteilung und Inhaltsausrichtung bereitstellte.<br><br><strong>So funktioniert es:</strong> Wenn du <kbd>display: flex</kbd> auf ein Element setzt, wird es zum <em>Flex-Container</em>. Seine direkten Kinder werden automatisch zu <em>Flex-Items</em>, die entlang einer Hauptachse fließen (standardmäßig horizontal). Diese eine Eigenschaft verwandelt gestapelte Block-Elemente in eine horizontale Reihe.<br><br><strong>Die zwei Achsen:</strong><br>• <em>Hauptachse</em> Die primäre Richtung, in der Items fließen (row = links→rechts)<br>• <em>Querachse</em> Senkrecht zur Hauptachse (row = oben→unten)<br><br><pre>.nav {\n display: flex;\n}</pre>",
"task": "Dieses Navigationsmenü stapelt sich vertikal. Füge <kbd>display: flex</kbd> hinzu, um die Links horizontal anzuordnen.",
"previewHTML": "<nav class=\"nav\"><a href=\"#\">Home</a><a href=\"#\">Products</a><a href=\"#\">About</a><a href=\"#\">Contact</a></nav>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .nav { background: #1a1a2e; padding: 1rem; } .nav a { color: white; text-decoration: none; padding: 8px 1rem; border-radius: 4px; } .nav a:hover { background: rgba(255,255,255,0.1); }",
"sandboxCSS": "",
"codePrefix": ".nav {\n ",
"title": "Flex Container",
"description": "Flexbox ist ein eindimensionales Layout-System. Mit <kbd>display: flex</kbd> wird ein Element zum Flex-Container. Alle direkten Kinder werden automatisch zu Flex-Items und richten sich horizontal (Hauptachse) aus. Die Querachse verläuft senkrecht dazu.",
"task": "Füge <kbd>display: flex</kbd> zu <kbd>.wrap</kbd> hinzu.",
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; }",
"codePrefix": ".wrap {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "display: flex;",
@@ -28,34 +28,41 @@
},
{
"id": "flexbox-2",
"title": "Gap",
"description": "Die <kbd>gap</kbd>-Eigenschaft fügt konsistenten Abstand zwischen Flex-Items hinzu, ohne dass Margins nötig sind. Sie erzeugt nur Platz zwischen Items, nicht an den Rändern.",
"task": "Füge <kbd>gap: 1rem</kbd> hinzu, um die Navigationslinks gleichmäßig zu verteilen.",
"previewHTML": "<nav class=\"nav\"><a href=\"#\">Home</a><a href=\"#\">Products</a><a href=\"#\">About</a><a href=\"#\">Contact</a></nav>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .nav { background: #1a1a2e; padding: 1rem; display: flex; } .nav a { color: white; text-decoration: none; padding: 8px 1rem; border-radius: 4px; background: rgba(255,255,255,0.1); }",
"sandboxCSS": "",
"codePrefix": ".nav {\n ",
"title": "Direction & Wrap",
"description": "<kbd>flex-direction</kbd> bestimmt die Hauptachse: <kbd>row</kbd> (horizontal, Standard) oder <kbd>column</kbd> (vertikal). Mit <kbd>flex-wrap: wrap</kbd> brechen Items in die nächste Zeile/Spalte um, wenn der Platz nicht reicht.",
"task": "Füge <kbd>flex-direction: column</kbd> und <kbd>flex-wrap: wrap</kbd> zu <kbd>.wrap</kbd> hinzu.",
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div><div class='box'>4</div><div class='box'>5</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; height: 16rem; display: flex; }",
"codePrefix": ".wrap {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "gap: 1rem;",
"solution": "flex-direction: column;\n flex-wrap: wrap;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "gap", "expected": "1rem" },
"message": "Setze <kbd>gap: 1rem</kbd>"
"value": { "property": "flex-direction", "expected": "column" },
"message": "Setze <kbd>flex-direction: column</kbd>",
"options": { "exact": true }
},
{
"type": "property_value",
"value": { "property": "flex-wrap", "expected": "wrap" },
"message": "Setze <kbd>flex-wrap: wrap</kbd>",
"options": { "exact": true }
}
]
},
{
"id": "flexbox-3",
"title": "Justify Content",
"description": "<kbd>justify-content</kbd> verteilt Items entlang der Hauptachse. Häufige Werte:<br>• <kbd>flex-start</kbd> Items am Anfang<br>• <kbd>flex-end</kbd> Items am Ende<br>• <kbd>center</kbd> Items zentrieren<br>• <kbd>space-between</kbd> Gleicher Abstand zwischen Items<br>• <kbd>space-around</kbd> Gleicher Abstand um Items",
"task": "Schiebe den \"Login\"-Button nach rechts, indem du <kbd>justify-content: space-between</kbd> auf die Navigation setzt.",
"previewHTML": "<nav class=\"nav\"><div class=\"links\"><a href=\"#\">Home</a><a href=\"#\">Products</a><a href=\"#\">About</a></div><a href=\"#\" class=\"login\">Login</a></nav>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .nav { background: #1a1a2e; padding: 1rem; display: flex; } .links { display: flex; gap: 8px; } .nav a { color: white; text-decoration: none; padding: 8px 1rem; border-radius: 4px; } .nav a:hover { background: rgba(255,255,255,0.1); } .login { background: steelblue; }",
"sandboxCSS": "",
"codePrefix": ".nav {\n ",
"description": "<kbd>justify-content</kbd> verteilt Items entlang der Hauptachse. Werte: <kbd>flex-start</kbd> (Anfang), <kbd>flex-end</kbd> (Ende), <kbd>center</kbd> (Mitte), <kbd>space-between</kbd> (gleichmäßig mit Abstand), <kbd>space-around</kbd> (gleichmäßig mit Rand).",
"task": "Füge <kbd>justify-content: space-between</kbd> zu <kbd>.wrap</kbd> hinzu.",
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; }",
"codePrefix": ".wrap {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "justify-content: space-between;",
@@ -64,19 +71,20 @@
{
"type": "property_value",
"value": { "property": "justify-content", "expected": "space-between" },
"message": "Setze <kbd>justify-content: space-between</kbd>"
"message": "Setze <kbd>justify-content: space-between</kbd>",
"options": { "exact": true }
}
]
},
{
"id": "flexbox-4",
"title": "Align Items",
"description": "<kbd>align-items</kbd> steuert die Ausrichtung auf der Querachse (vertikal bei flex-direction: row). Werte sind:<br>• <kbd>stretch</kbd> Ausdehnen zum Füllen (Standard)<br>• <kbd>flex-start</kbd> Oben ausrichten<br>• <kbd>flex-end</kbd> Unten ausrichten<br>• <kbd>center</kbd> Vertikal zentrieren",
"task": "Das Logo und die Nav-Links haben unterschiedliche Höhen. Zentriere sie vertikal mit <kbd>align-items: center</kbd>.",
"previewHTML": "<header class=\"header\"><div class=\"logo\">ACME</div><nav><a href=\"#\">Products</a><a href=\"#\">Pricing</a><a href=\"#\">Docs</a></nav></header>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .header { background: white; padding: 1rem 2rem; display: flex; justify-content: space-between; border-bottom: 1px solid #eee; } .logo { font-size: 1.5rem; font-weight: bold; color: steelblue; } nav { display: flex; gap: 1rem; } nav a { color: #333; text-decoration: none; font-size: 0.9rem; }",
"sandboxCSS": "",
"codePrefix": ".header {\n ",
"description": "<kbd>align-items</kbd> richtet Items entlang der Querachse aus (bei <kbd>row</kbd>: vertikal). Werte: <kbd>stretch</kbd> (Standard, füllt Höhe), <kbd>flex-start</kbd> (oben), <kbd>flex-end</kbd> (unten), <kbd>center</kbd> (Mitte), <kbd>baseline</kbd> (Textlinie).",
"task": "Füge <kbd>align-items: center</kbd> zu <kbd>.wrap</kbd> hinzu.",
"previewHTML": "<div class='wrap'><div class='box tall'>1</div><div class='box'>2</div><div class='box short'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; display: flex; justify-content: center; } .tall { height: 6rem; } .short { height: 3rem; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; height: 10rem; }",
"codePrefix": ".wrap {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "align-items: center;",
@@ -85,49 +93,50 @@
{
"type": "property_value",
"value": { "property": "align-items", "expected": "center" },
"message": "Setze <kbd>align-items: center</kbd>"
"message": "Setze <kbd>align-items: center</kbd>",
"options": { "exact": true }
}
]
},
{
"id": "flexbox-5",
"title": "Flex Wrap",
"description": "Standardmäßig quetschen sich Flex-Items in eine Zeile. <kbd>flex-wrap: wrap</kbd> erlaubt Items, auf mehrere Zeilen umzubrechen, wenn der Platz nicht reicht.",
"task": "Diese Karten laufen über den Container hinaus. Füge <kbd>flex-wrap: wrap</kbd> hinzu, damit sie in neue Zeilen umbrechen können.",
"previewHTML": "<div class=\"cards\"><article class=\"card\">Card 1</article><article class=\"card\">Card 2</article><article class=\"card\">Card 3</article><article class=\"card\">Card 4</article><article class=\"card\">Card 5</article><article class=\"card\">Card 6</article></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .cards { display: flex; gap: 1rem; } .card { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); min-width: 120px; text-align: center; }",
"sandboxCSS": "",
"codePrefix": ".cards {\n ",
"title": "Flex Grow",
"description": "Die <kbd>flex</kbd>-Eigenschaft ist eine Kurzform für <kbd>flex-grow</kbd>, <kbd>flex-shrink</kbd> und <kbd>flex-basis</kbd>. Ein höherer Wert bedeutet, dass das Element mehr vom verfügbaren Platz einnimmt. <kbd>flex: 2</kbd> wächst doppelt so schnell wie <kbd>flex: 1</kbd>.",
"task": "Füge <kbd>flex: 2</kbd> zu <kbd>.box2</kbd> hinzu.",
"previewHTML": "<div class='wrap'><div class='box box1'>1</div><div class='box box2'>2</div><div class='box box3'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; display: flex; align-items: center; justify-content: center; } .box1 { background: coral; flex: 1; } .box2 { background: mediumseagreen; } .box3 { background: gold; flex: 1; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; }",
"codePrefix": ".box2 {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "flex-wrap: wrap;",
"solution": "flex: 2;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "flex-wrap", "expected": "wrap" },
"message": "Setze <kbd>flex-wrap: wrap</kbd>"
"value": { "property": "flex", "expected": "2" },
"message": "Setze <kbd>flex: 2</kbd>"
}
]
},
{
"id": "flexbox-6",
"title": "Flex Grow",
"description": "Die <kbd>flex</kbd>-Eigenschaft auf Items steuert, wie sie wachsen und schrumpfen. <kbd>flex: 1</kbd> lässt ein Item wachsen, um verfügbaren Platz zu füllen. Mehrere Items mit <kbd>flex: 1</kbd> teilen sich den Platz gleichmäßig.",
"task": "Lass das Suchfeld den verfügbaren Platz ausfüllen, indem du <kbd>flex: 1</kbd> auf <kbd>.search</kbd> setzt.",
"previewHTML": "<div class=\"toolbar\"><input class=\"search\" type=\"text\" placeholder=\"Search...\"><button class=\"btn\">Search</button><button class=\"btn\">Filters</button></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .toolbar { display: flex; gap: 8px; padding: 1rem; background: #f5f5f5; border-radius: 8px; } .search { padding: 8px 1rem; border: 1px solid #ddd; border-radius: 4px; font-size: 1rem; } .btn { padding: 8px 1rem; background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; }",
"sandboxCSS": "",
"codePrefix": ".search {\n ",
"title": "Align Self",
"description": "<kbd>align-self</kbd> überschreibt <kbd>align-items</kbd> für ein einzelnes Element. So kannst du ein Item individuell auf der Querachse positionieren, während alle anderen Items ihrer Standard-Ausrichtung folgen.",
"task": "Füge <kbd>align-self: flex-start</kbd> zu <kbd>.middle</kbd> hinzu.",
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box middle'>2</div><div class='box'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; } .middle { background: mediumseagreen; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; height: 12rem; align-items: center; }",
"codePrefix": ".middle {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "flex: 1;",
"solution": "align-self: flex-start;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "flex", "expected": "1" },
"message": "Setze <kbd>flex: 1</kbd>"
"value": { "property": "align-self", "expected": "flex-start" },
"message": "Setze <kbd>align-self: flex-start</kbd>"
}
]
}

View File

@@ -1,257 +1,548 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "css-basic-selectors",
"title": "Fundamentos de CSS",
"description": "Aprende los bloques fundamentales de CSS: propiedades, valores y selectores. Este módulo te enseña las reglas de sintaxis que sigue cada declaración CSS.",
"title": "CSS Selectors",
"description": "CSS selectors are the foundation of styling web pages, allowing you to target specific HTML elements for styling. This module introduces fundamental selector types including element type selectors, class selectors, ID selectors, and the universal selector.",
"difficulty": "beginner",
"lessons": [
{
"id": "css-properties",
"title": "Propiedades CSS",
"description": "CSS estiliza elementos usando <strong>declaraciones</strong> - pares de propiedades y valores. Cada declaración sigue el mismo patrón:<br><br><pre>property: value;</pre><br>La <strong>propiedad</strong> es lo que quieres cambiar (como <kbd>color</kbd> o <kbd>background</kbd>). El <strong>valor</strong> es a lo que lo estableces. Dos puntos los separan, y un punto y coma termina la línea.<br><br>Los valores vienen en diferentes tipos:<br>• <strong>Palabras clave:</strong> <kbd>red</kbd>, <kbd>bold</kbd>, <kbd>center</kbd><br>• <strong>Números con unidades:</strong> <kbd>16px</kbd>, <kbd>2rem</kbd>, <kbd>100%</kbd><br>• <strong>Colores:</strong> <kbd>steelblue</kbd>, <kbd>#ff0000</kbd>",
"task": "Completa la declaración añadiendo <kbd>color: coral;</kbd> para cambiar el color del texto.",
"previewHTML": "<p class=\"text\">This text should turn coral.</p>",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } .text { font-size: 1.25rem; }",
"sandboxCSS": "",
"codePrefix": ".text {\n ",
"id": "introduction-to-selectors",
"title": "What's a Selector?",
"description": "A CSS selector is the first part of a CSS rule that tells the browser which HTML elements should receive the styles defined in the declaration block. Selectors are essentially patterns that match against elements in your HTML document. Understanding selectors is fundamental because they determine which elements your CSS rules will affect. The element or elements targeted by a selector are referred to as the 'subject of the selector.' When writing a CSS rule, you first specify the selector, followed by curly braces that contain the style declarations.<br/>For example, to change the text color of elements, you can use the <kbd>color</kbd> property within your declaration block.<br><br><pre>/* Element selector */\np {\n color: orangered;\n /* │ └─── Indicates the value of the expression\n │ \n └─────────── Indicates the property of the expression */\n}</pre>",
"task": "Write a CSS rule using a type selector that targets all paragraph elements <kbd>p</kbd> in the document. Make the text blue by setting the <kbd>color</kbd> property to <kbd>blue</kbd>.",
"previewHTML": "<h1>Introduction to CSS Selectors</h1>\n<p>This paragraph should turn blue.</p>\n<div>This div element should remain unchanged.</div>\n<p>This second paragraph should also turn blue.</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h1, p, div { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
"codePrefix": "/* Write a type selector to target all paragraph elements */\n",
"initialCode": "",
"codeSuffix": "\n}",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "color: coral;",
"solution": "p { color: blue }",
"validations": [
{
"type": "property_value",
"value": { "property": "color", "expected": "coral" },
"message": "Añade <kbd>color: coral;</kbd>"
}
]
},
{
"id": "multiple-properties",
"title": "Múltiples propiedades",
"description": "Una regla puede tener múltiples declaraciones. Cada una va en su propia línea, y cada una necesita un punto y coma al final:<br><br><pre>.box {<br> background: gold;<br> color: navy;<br> padding: 1rem;<br>}</pre><br>El orden normalmente no importa - CSS las aplica todas. Cuando hay conflictos, la última gana.",
"task": "Añade dos declaraciones: <kbd>background: lavender;</kbd> y <kbd>padding: 1rem;</kbd>",
"previewHTML": "<div class=\"card\">A styled card with background and padding.</div>",
"previewBaseCSS": "body { font-family: system-ui; padding: 20px; } .card { border-radius: 8px; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"previewContainer": "preview-area",
"solution": "background: lavender;\n padding: 1rem;",
"validations": [
"type": "regex",
"value": "^p\\s*{",
"message": "Start your rule with <kbd>p { … }</kbd> to select all paragraph elements",
"options": {
"caseSensitive": false
}
},
{
"type": "property_value",
"value": { "property": "background", "expected": "lavender" },
"message": "Añade <kbd>background: lavender;</kbd>"
"type": "contains",
"value": "color:",
"message": "Include the <kbd>color:</kbd> property in your CSS rule"
},
{
"type": "contains",
"value": "blue",
"message": "Set the color value to <kbd>blue</kbd>"
},
{
"type": "property_value",
"value": { "property": "padding", "expected": "1rem" },
"message": "Añade <kbd>padding: 1rem;</kbd>"
"value": {
"property": "color",
"expected": "blue"
},
"message": "Use <kbd>color: blue</kbd> to set the text color"
},
{
"type": "regex",
"value": "p\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": false
}
}
]
},
{
"id": "type-selectors",
"title": "Selectores de tipo",
"description": "Un <strong>selector</strong> le dice al navegador qué elementos estilizar. El selector más simple es un <strong>selector de tipo</strong> — simplemente el nombre de la etiqueta HTML.<br><br><pre>p {<br> color: steelblue;<br>}</pre><br>Esta regla apunta a cada elemento <kbd>&lt;p&gt;</kbd> en la página. Los selectores de tipo son geniales para establecer estilos base.",
"task": "Estiliza todos los párrafos. Escribe una regla con <kbd>p</kbd> como selector y establece <kbd>color: steelblue</kbd>.",
"previewHTML": "<article>\n <h2>Fresh Roasted Coffee</h2>\n <p>Our beans are sourced from small farms in Colombia and Ethiopia.</p>\n <p>Each batch is roasted weekly to ensure peak freshness.</p>\n</article>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } h2 { margin: 0 0 1rem; }",
"sandboxCSS": "",
"codePrefix": "",
"initialCode": "",
"title": "Type Selectors",
"description": "Type selectors (also called tag name selectors or element selectors) target HTML elements based on their tag name. For example, <kbd>p</kbd> selects all paragraph elements, <kbd>h1</kbd> selects all level-one headings, and <kbd>div</kbd> selects all division elements. Type selectors are the most fundamental way to select elements, applying styles consistently to all instances of a particular HTML element throughout your document. You can define a variety of CSS properties with type selectors, such as <kbd>color</kbd> for text color, <kbd>background-color</kbd> for the background, and <kbd>font-weight</kbd> for text emphasis. They provide a broad approach for styling your page and are often the starting point for more specific styling using other selector types.",
"task": "Write three separate CSS rules using type selectors to target specific HTML elements: make <kbd>h2</kbd> headings <kbd>purple</kbd>, give <kbd>span</kbd> elements a <kbd>yellow</kbd> background, and make <kbd>strong</kbd> elements <kbd>red</kbd>.",
"previewHTML": "<h2>Type Selectors Example</h2>\n<p>Regular paragraph text <span>with a highlighted span</span> that should have a yellow background.</p>\n<p>Another paragraph with <strong>strong important text</strong> that should be red.</p>\n<h2>Another Heading</h2>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h2, p, span, strong { padding: 3px; }",
"codePrefix": "/* Write three separate type selectors below */\n\n",
"initialCode": "/* 1. Make h2 headings purple */\n\n\n/* 2. Give span elements a yellow background */\n\n\n/* 3. Make strong elements red */\n",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "p {\n color: steelblue;\n}",
"solution": "/* 1. Make h2 headings purple */\nh2 {\n color: purple;\n}\n\n/* 2. Give span elements a yellow background */\nspan {\n background-color: yellow;\n}\n\n/* 3. Make strong elements red */\nstrong {\n color: red;\n}",
"validations": [
{
"type": "regex",
"value": "p\\s*\\{",
"message": "Empieza con <kbd>p {</kbd> para seleccionar párrafos"
"value": "^h2\\s*{",
"message": "Include an <kbd>h2 { … }</kbd> selector"
},
{
"type": "property_value",
"value": { "property": "color", "expected": "steelblue" },
"message": "Establece <kbd>color: steelblue</kbd>"
}
]
},
{
"id": "styling-links",
"title": "Estilizando enlaces",
"description": "Los selectores de tipo funcionan para cualquier elemento HTML. El selector <kbd>a</kbd> apunta a todos los enlaces de una página.<br><br>Los enlaces tienen un color azul y subrayado por defecto. Puedes cambiar ambos con CSS — usa <kbd>color</kbd> para el texto y <kbd>text-decoration: none</kbd> para quitar el subrayado.",
"task": "Estiliza los enlaces de navegación. Escribe una regla con <kbd>a</kbd> como selector y establece <kbd>color: coral</kbd>.",
"previewHTML": "<nav>\n <a href=\"#\">Home</a>\n <a href=\"#\">Menu</a>\n <a href=\"#\">About</a>\n <a href=\"#\">Contact</a>\n</nav>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } nav { display: flex; gap: 1.5rem; }",
"sandboxCSS": "",
"codePrefix": "",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "a {\n color: coral;\n}",
"validations": [
"value": {
"property": "color",
"expected": "purple"
},
"message": "Set the <kbd>color</kbd> property to <kbd>purple</kbd> for h2 elements"
},
{
"type": "regex",
"value": "a\\s*\\{",
"message": "Empieza con <kbd>a {</kbd> para seleccionar enlaces"
"value": "h2\\s*{[^}]*}",
"message": "Make sure to close your h2 rule with a closing brace <kbd>}</kbd>"
},
{
"type": "regex",
"value": "^span\\s*{",
"message": "Include a <kbd>span { … }</kbd> selector"
},
{
"type": "property_value",
"value": { "property": "color", "expected": "coral" },
"message": "Establece <kbd>color: coral</kbd>"
"value": {
"property": "background-color",
"expected": "yellow"
},
"message": "Set a <kbd>background-color: yellow</kbd> for span elements"
},
{
"type": "regex",
"value": "span\\s*{[^}]*}",
"message": "Make sure to close your span rule with a closing brace <kbd>}</kbd>"
},
{
"type": "regex",
"value": "^strong\\s*{",
"message": "Include a <kbd>strong { … }</kbd> selector"
},
{
"type": "regex",
"value": "strong\\s*{\\s*color:\\s*red;[^}]*}",
"message": "Set the <kbd>color: red</kbd> for strong elements"
}
]
},
{
"id": "class-selectors",
"title": "Selectores de clase",
"description": "Los selectores de tipo estilizan <em>todos</em> los elementos de ese tipo. ¿Pero qué si quieres estilizar solo algunos de ellos?<br><br>Los <strong>selectores de clase</strong> apuntan a elementos con un atributo <kbd>class</kbd> específico. Empiezan con un punto:<br><br><pre>.badge {<br> background: coral;<br>}</pre><br>Esto estiliza solo elementos con <kbd>class=\"badge\"</kbd>.",
"task": "Estiliza el badge de notificación. Escribe una regla con <kbd>.badge</kbd> como selector y establece <kbd>background: tomato</kbd>.",
"previewHTML": "<header>\n <h1>Dashboard</h1>\n <span class=\"badge\">3</span>\n</header>\n<p>You have new notifications waiting.</p>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem; } h1 { margin: 0; font-size: 1.5rem; } .badge { color: white; padding: 0.25rem 0.5rem; border-radius: 999px; font-size: 0.875rem; } p { color: #555; margin: 0; }",
"sandboxCSS": "",
"codePrefix": "",
"title": "Class Selectors",
"description": "Class selectors target elements with a specific class attribute value. They begin with a dot (.) followed by the class name. Classes are powerful because they allow you to apply the same styles to multiple elements regardless of their type. An HTML element can have multiple classes (separated by spaces in the class attribute), and a class can be applied to any number of elements. When using class selectors, you can apply properties like <kbd>background-color</kbd> to set the background color of elements, and <kbd>font-weight</kbd> to control text thickness, making text bold or normal. This flexibility makes class selectors one of the most commonly used methods for applying styles in CSS, allowing for modular and reusable styling across your website.",
"task": "Create a CSS rule using a class selector that targets elements with the class <kbd>highlight</kbd>. Give these elements a <kbd>yellow</kbd> background and <kbd>bold</kbd> text.",
"previewHTML": "<h2>Using Class Selectors</h2>\n<p>This is a regular paragraph, but <span class=\"highlight\">this span has the highlight class</span> applied to it.</p>\n<p class=\"highlight\">This entire paragraph has the highlight class.</p>\n<ul>\n <li>Regular list item</li>\n <li class=\"highlight\">This list item is highlighted</li>\n</ul>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h2, p, li { padding: 5px; margin-bottom: 10px; }",
"codePrefix": "/* Create a class selector for elements with the 'highlight' class */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": ".badge {\n background: tomato;\n}",
"validations": [
{
"type": "regex",
"value": "\\.badge\\s*\\{",
"message": "Empieza con <kbd>.badge {</kbd> (¡no olvides el punto!)"
"value": "^\\.highlight\\s*{",
"message": "Start your rule with <kbd>.highlight { … }</kbd> to create a class selector",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "background-color:",
"message": "Include the <kbd>background-color:</kbd> property"
},
{
"type": "property_value",
"value": { "property": "background", "expected": "tomato" },
"message": "Establece <kbd>background: tomato</kbd>"
"value": {
"property": "background-color",
"expected": "yellow"
},
"message": "Set the background color to <kbd>yellow</kbd>"
},
{
"type": "contains",
"value": "font-weight:",
"message": "Include the <kbd>font-weight:</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "font-weight",
"expected": "bold"
},
"message": "Set the font-weight to <kbd>bold</kbd>"
},
{
"type": "regex",
"value": "\\.highlight\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "button-variants",
"title": "Variantes de botón",
"description": "Los elementos pueden tener múltiples clases. Cuando encadenas selectores de clase sin espacios, apuntas a elementos que tienen <em>todas</em> esas clases:<br><br><pre>.btn.primary {<br> background: steelblue;<br>}</pre><br>Esto apunta a elementos con <kbd>class=\"btn primary\"</kbd>, no solo <kbd>.btn</kbd> o solo <kbd>.primary</kbd>.",
"task": "Estiliza el botón primario. Escribe una regla con <kbd>.btn.primary</kbd> como selector y establece <kbd>background: steelblue</kbd>.",
"previewHTML": "<div class=\"actions\">\n <button class=\"btn\">Cancel</button>\n <button class=\"btn primary\">Save Changes</button>\n</div>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } .actions { display: flex; gap: 0.75rem; } .btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 1rem; cursor: pointer; background: #e0e0e0; color: #333; }",
"id": "multiple-classes",
"title": "Multiple Classes",
"description": "HTML elements can have multiple classes applied simultaneously, allowing for composable and modular CSS designs. When an element has multiple classes, it will receive styles from all matching class selectors. This approach enables you to build a library of reusable CSS classes that can be combined in different ways. You can also target elements that have a specific combination of classes by chaining class selectors together without spaces (e.g., <kbd>.class1.class2</kbd>). When styling these elements, you might use properties like <kbd>border-color</kbd> to change the color of element borders, and <kbd>background-color</kbd> to set the background color of elements. This technique lets you create conditional styles that only apply when certain classes appear together.",
"task": "Complete the CSS rule that targets elements with both <kbd>card</kbd> and <kbd>featured</kbd> classes by chaining the selectors. Set the border-color to gold and the background-color to lemonchiffon to make featured cards stand out.",
"previewHTML": "<h2>Multiple Class Combinations</h2>\n<div class=\"card\">Regular Card</div>\n<div class=\"card featured\">Featured Card</div>\n<div class=\"featured\">Just Featured (not a card)</div>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .card { border: 2px solid gray; padding: 15px; margin-bottom: 10px; border-radius: 5px; }",
"sandboxCSS": "",
"codePrefix": "",
"codePrefix": "/* The .card class already has basic styling */\n/* Now target elements with BOTH classes: 'card' AND 'featured' */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": ".btn.primary {\n background: steelblue;\n}",
"solution": ".card.featured { border-color: gold; background-color: lemonchiffon }",
"validations": [
{
"type": "regex",
"value": "\\.btn\\.primary\\s*\\{",
"message": "Usa <kbd>.btn.primary {</kbd> (sin espacio entre clases)"
"value": "^\\.card\\.featured\\s*{",
"message": "Chain the selectors as <kbd>.card.featured</kbd> (no space between them)",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "border-color:",
"message": "Include the <kbd>border-color</kbd> property"
},
{
"type": "property_value",
"value": { "property": "background", "expected": "steelblue" },
"message": "Establece <kbd>background: steelblue</kbd>"
"value": {
"property": "border-color",
"expected": "gold"
},
"message": "Set the border color to <kbd>gold</kbd>"
},
{
"type": "regex",
"value": "\\.card\\.featured\\s*{[^}]*;",
"message": "Make sure to end your CSS rule with a semicolon <kbd>;</kbd>"
},
{
"type": "contains",
"value": "background-color:",
"message": "Include the <kbd>background-color</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "background-color",
"expected": "lemonchiffon"
},
"message": "Set the background color to <kbd>lemonchiffon</kbd>"
},
{
"type": "regex",
"value": "\\.card\\.featured\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "specific-elements",
"title": "Apuntando a elementos específicos",
"description": "A veces quieres que una clase se vea diferente en diferentes elementos. Combina un selector de tipo con un selector de clase (sin espacio) para ser más específico:<br><br><pre>a.btn {<br> text-decoration: none;<br>}</pre><br>Esto estiliza solo elementos <kbd>&lt;a&gt;</kbd> con la clase <kbd>btn</kbd>, no elementos <kbd>&lt;button&gt;</kbd> con esa clase.",
"task": "Quita el subrayado de los botones-enlace. Escribe una regla con <kbd>a.btn</kbd> como selector y establece <kbd>text-decoration: none</kbd>.",
"previewHTML": "<div class=\"actions\">\n <button class=\"btn\">Regular Button</button>\n <a href=\"#\" class=\"btn\">Link Button</a>\n</div>",
"previewBaseCSS": "body { font-family: system-ui; padding: 1rem; } .actions { display: flex; gap: 0.75rem; align-items: center; } .btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 1rem; cursor: pointer; background: steelblue; color: white; }",
"sandboxCSS": "",
"codePrefix": "",
"id": "class-with-type",
"title": "Combining Types",
"description": "You can combine type selectors with class selectors to target specific HTML elements that have a certain class. This creates a more specific selector that only matches when both conditions are true: the element is of the specified type AND it has the specified class. For example, <kbd>p.note</kbd> would select paragraph elements with the class <kbd>note</kbd>, but would not select divs or spans with that same class. You can style these combined selections using properties like <kbd>background-color</kbd> to set a colored background for your elements. This approach allows you to apply different styles to the same class when it appears on different element types.",
"task": "Create a CSS rule that specifically targets <kbd>&lt;span&gt;</kbd> elements with the class <kbd>highlight</kbd>. Make those elements have an orange background, while other elements with the highlight class remain untouched.",
"previewHTML": "<h2>Type and Class Combinations</h2>\n<p>This paragraph has a <span class=\"highlight\">highlighted span</span> that should have an orange background.</p>\n<p class=\"highlight\">This paragraph has the highlight class but should NOT have an orange background.</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } .highlight { font-weight: bold; }",
"sandboxCSS": "h2, p, span { padding: 5px; }",
"codePrefix": "/* The .highlight class already sets font-weight to bold */\n/* Now target ONLY span elements with the highlight class */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "a.btn {\n text-decoration: none;\n}",
"validations": [
{
"type": "regex",
"value": "a\\.btn\\s*\\{",
"message": "Usa <kbd>a.btn {</kbd> (tipo + clase, sin espacio)"
"value": "^span\\.highlight\\s*{",
"message": "Use <kbd>span.highlight</kbd> selector (no space between element and class)",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "background-color:",
"message": "Include the <kbd>background-color</kbd> property"
},
{
"type": "property_value",
"value": { "property": "text-decoration", "expected": "none" },
"message": "Establece <kbd>text-decoration: none</kbd>"
"value": {
"property": "background-color",
"expected": "orange"
},
"message": "Set the background color to <kbd>orange</kbd>"
},
{
"type": "regex",
"value": "span\\.highlight\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "grouping-selectors",
"title": "Agrupando selectores",
"description": "Cuando múltiples elementos necesitan los mismos estilos, lístalos separados por comas. Esto mantiene tu CSS limpio y mantenible.<br><br><pre>h1, h2, h3 {<br> color: steelblue;<br>}</pre><br>Esto aplica el mismo color a los tres niveles de encabezado en una regla.",
"task": "Estiliza todos los encabezados consistentemente. Añade <kbd>color: steelblue</kbd> al selector agrupado <kbd>h1, h2, h3</kbd>.",
"previewHTML": "<article><h1>Main Title</h1><p>Introduction paragraph with some text.</p><h2>Section Heading</h2><p>More content here.</p><h3>Subsection</h3><p>Final paragraph.</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } article { max-width: 500px; } p { color: #555; line-height: 1.6; }",
"sandboxCSS": "",
"codePrefix": "h1, h2, h3 {\n ",
"id": "id-selectors",
"title": "ID Selectors",
"description": "ID selectors target elements with a specific id attribute. They begin with a hash/pound sign (#) followed by the ID name. Unlike classes, IDs must be unique within a document—each ID value should be used only once per page. ID selectors have higher specificity than class or element selectors, meaning they override those selectors when conflicts arise. When styling with ID selectors, you can use properties like <kbd>color</kbd> to define text color, and <kbd>text-decoration</kbd> to control the appearance of text, such as adding underlines to elements. Because of their uniqueness requirement, IDs are best used for one-of-a-kind elements like page headers, main navigation, or specific unique components that appear only once on a page.",
"task": "Create a CSS rule with an ID selector that targets the element with the ID <kbd>main-title</kbd>. Set its color to purple and add an underline with <kbd>text-decoration: underline</kbd>.",
"previewHTML": "<h1 id=\"main-title\">Main Page Title</h1>\n<p>Regular paragraph content.</p>\n<h2>Secondary Heading</h2>\n<p id=\"intro\">Introduction paragraph (different ID).</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h1, h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
"codePrefix": "/* Create an ID selector to target the element with id=\"main-title\" */\n",
"initialCode": "",
"codeSuffix": "\n}",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": "color: steelblue;",
"validations": [
{
"type": "regex",
"value": "^#main-title\\s*{",
"message": "Start your rule with <kbd>#main-title</kbd> to create an ID selector",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "color:",
"message": "Include the <kbd>color</kbd> property"
},
{
"type": "property_value",
"value": { "property": "color", "expected": "steelblue" },
"message": "Establece <kbd>color: steelblue</kbd>"
"value": {
"property": "color",
"expected": "purple"
},
"message": "Set the color to <kbd>purple</kbd>"
},
{
"type": "contains",
"value": "text-decoration:",
"message": "Include the <kbd>text-decoration</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "text-decoration",
"expected": "underline"
},
"message": "Set the text-decoration to <kbd>underline</kbd>"
},
{
"type": "regex",
"value": "#main-title\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "descendant-selectors",
"title": "Selectores descendientes",
"description": "Apunta a elementos dentro de otros elementos usando un espacio entre selectores. Este es uno de los patrones más útiles en CSS.<br><br><pre>.nav a {<br> color: white;<br>}</pre><br>Esto estiliza solo enlaces dentro de <kbd>.nav</kbd>, dejando otros enlaces sin cambios.",
"task": "Estiliza los enlaces de navegación diferente. Escribe una regla con <kbd>.nav a</kbd> como selector y establece <kbd>color: white</kbd>.",
"previewHTML": "<nav class=\"nav\"><a href=\"#\">Home</a><a href=\"#\">About</a><a href=\"#\">Contact</a></nav><p>Read more in our <a href=\"#\">documentation</a>.</p>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; margin: 0; } .nav { background: steelblue; padding: 1rem; display: flex; gap: 1rem; border-radius: 8px; margin-bottom: 1rem; } .nav a { text-decoration: none; } p a { color: steelblue; }",
"sandboxCSS": "",
"codePrefix": "",
"id": "id-with-type",
"title": "Type + ID",
"description": "Similar to how you can combine type and class selectors, you can also combine type selectors with ID selectors. For example, <kbd>h1#title</kbd> targets an h1 element with the ID 'title'. When using this combined approach, you can apply CSS properties like <kbd>font-style</kbd> to control the slant of the text, making it italic or normal. While this selector combination is more specific than using just the ID selector, it's often unnecessary since IDs should already be unique in a document. However, this technique can be useful for improving code readability or when you want to emphasize that a particular ID should only appear on a specific element type.",
"task": "Create a CSS rule that combines a type selector with an ID selector to target specifically a paragraph element with the ID <kbd>special</kbd>. Set its font style to italic.",
"previewHTML": "<h2 id=\"special\">Heading with ID \"special\" (should NOT be affected)</h2>\n<p id=\"special\">Paragraph with ID \"special\" (should become italic)</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "h2, p { padding: 8px; margin-bottom: 10px; border: 1px dashed #ccc; }",
"codePrefix": "/* Create a combined type+ID selector for a paragraph with id=\"special\" */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": ".nav a {\n color: white;\n}",
"validations": [
{
"type": "regex",
"value": "\\.nav\\s+a\\s*\\{",
"message": "Usa <kbd>.nav a {</kbd> (espacio entre .nav y a)"
"value": "^p#special\\s*{",
"message": "Use <kbd>p#special</kbd> to target paragraphs with ID <kbd>special</kbd>",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "font-style:",
"message": "Include the <kbd>font-style</kbd> property"
},
{
"type": "property_value",
"value": { "property": "color", "expected": "white" },
"message": "Establece <kbd>color: white</kbd>"
"value": {
"property": "font-style",
"expected": "italic"
},
"message": "Set the font-style to <kbd>italic</kbd>"
},
{
"type": "regex",
"value": "p#special\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "nested-styling",
"title": "Estilos anidados",
"description": "Los selectores descendientes te permiten crear estilos contextuales. El mismo elemento puede verse diferente dependiendo de dónde aparezca.<br><br>Por ejemplo, los párrafos en una <kbd>.card</kbd> podrían ser más pequeños que los párrafos en un <kbd>article</kbd>.",
"task": "Haz los párrafos dentro de la tarjeta más pequeños. Escribe una regla con <kbd>.card p</kbd> como selector y establece <kbd>font-size: 0.9rem</kbd>.",
"previewHTML": "<article><h2>Article Title</h2><p>This is a regular article paragraph with normal-sized text for comfortable reading.</p><div class=\"card\"><strong>Quick Tip</strong><p>Card paragraphs should be slightly smaller to fit the compact design.</p></div><p>Back to regular article text here.</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } article { max-width: 500px; } h2 { color: steelblue; margin-top: 0; } p { line-height: 1.6; color: #444; } .card { background: #f0f4f8; padding: 1rem; border-radius: 8px; border-left: 4px solid steelblue; } .card strong { color: steelblue; display: block; margin-bottom: 0.5rem; }",
"sandboxCSS": "",
"codePrefix": "",
"id": "selector-lists",
"title": "Selector Lists",
"description": "When multiple elements need the same styling, you can group them together using a selector list (also known as grouping selectors). Selector lists are created by separating individual selectors with commas. This approach reduces repetition in your CSS, making it more maintainable and efficient. For example, <kbd>h1, h2, h3 { color: blue; }</kbd> applies the same blue color to all three heading levels. When styling multiple selectors at once, you can apply properties like <kbd>background-color</kbd> to set the background, <kbd>border-left</kbd> to create a left border with a specific thickness, style, and color, and <kbd>padding-left</kbd> to create space between the content and the left border. Whitespace around commas is optional, and each selector in the list can be any valid selector type-elements, classes, IDs, or even more complex selectors.",
"task": "Create a selector list that applies the same styles to three different elements: paragraphs with class <kbd>note</kbd>, list items with class <kbd>important</kbd>, and the element with ID <kbd>summary</kbd>. Give them a <kbd>lightyellow</kbd> background, a <kbd>gold</kbd> left border, and some left <kbd>padding</kbd>.",
"previewHTML": "<p class=\"note\">This is a note paragraph.</p>\n<ul>\n <li>Regular list item</li>\n <li class=\"important\">Important list item</li>\n</ul>\n<div id=\"summary\">Summary section</div>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "p, li, div { padding: 8px; margin-bottom: 8px; border: 1px dashed gray; }",
"codePrefix": "/* Create a selector list to apply the same styles to multiple different elements */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"solution": ".card p {\n font-size: 0.9rem;\n}",
"solution": "p.note,\nli.important,\n#summary {\n background-color: lightyellow;\n border-left: 3px solid gold;\n padding-left: 10px\n}",
"validations": [
{
"type": "contains",
"value": "p.note",
"message": "Include <kbd>p.note</kbd> in your selector list",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "li.important",
"message": "Include <kbd>li.important</kbd> in your selector list",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "#summary",
"message": "Include <kbd>#summary</kbd> in your selector list",
"options": {
"caseSensitive": true
}
},
{
"type": "regex",
"value": "\\.card\\s+p\\s*\\{",
"message": "Usa <kbd>.card p {</kbd> (espacio entre .card y p)"
"value": "(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)\\s*,\\s*(p\\.note|li\\.important|#summary)",
"message": "Create a comma-separated list with all three selectors",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "background-color:",
"message": "Include the <kbd>background-color</kbd> property"
},
{
"type": "property_value",
"value": { "property": "font-size", "expected": "0.9rem" },
"message": "Establece <kbd>font-size: 0.9rem</kbd>"
"value": {
"property": "background-color",
"expected": "lightyellow"
},
"message": "Set the background color to <kbd>lightyellow</kbd>"
},
{
"type": "contains",
"value": "border-left:",
"message": "Include the <kbd>border-left</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "border-left",
"expected": "3px solid gold"
},
"message": "Use <kbd>border-left: 3px solid gold</kbd> to create a left border"
},
{
"type": "contains",
"value": "padding-left:",
"message": "Include the <kbd>padding-left</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "padding-left",
"expected": "10px"
},
"message": "Use <kbd>padding-left: 10px</kbd> to add left padding"
}
]
},
{
"id": "universal-selector",
"title": "Universal (*)",
"description": "The universal selector is denoted by an asterisk (*) and matches any element of any type. It selects everything in the document or, when combined with other selectors, everything within a specific context. For example, <kbd>* { margin: 0; }</kbd> removes margins from all elements, while <kbd>article *</kbd> selects all elements inside article elements. When using the universal selector in combination with other selectors, you can apply properties like <kbd>margin</kbd> to control the spacing around elements. The universal selector is powerful but should be used carefully due to its broad impact. It's commonly used in CSS resets, to override default browser styling, or to target all children of a particular element.",
"task": "Use the universal selector to remove margins from all elements inside the container div. Create a rule using <kbd>div.container *</kbd> as the selector and set <kbd>margin: 0</kbd>.",
"previewHTML": "<div class=\"container\">\n <h2>Inside Container</h2>\n <p>This paragraph is inside the container.</p>\n <ul>\n <li>List item inside container</li>\n </ul>\n</div>\n<p>This paragraph is outside the container and should not be affected.</p>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; } div.container { border: 2px solid navy; padding: 15px; background-color: lavender; } h2, p, ul, li { margin: 15px 0; }",
"sandboxCSS": "",
"codePrefix": "/* Use the universal selector to target all elements inside the container */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "^div\\.container\\s+\\*\\s*{",
"message": "Use <kbd>div.container *</kbd> selector (with a space between container and *)",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "margin:",
"message": "Include the <kbd>margin</kbd> property"
},
{
"type": "property_value",
"value": {
"property": "margin",
"expected": "0"
},
"message": "Set margin to <kbd>0</kbd>"
},
{
"type": "regex",
"value": "div\\.container\\s+\\*\\s*{[^}]*}",
"message": "Make sure to close your CSS rule with a closing brace <kbd>}</kbd>",
"options": {
"caseSensitive": true
}
}
]
},
{
"id": "specificity-basics",
"title": "Specificity",
"description": "CSS specificity determines which styles take precedence when multiple conflicting rules target the same element. Specificity follows a hierarchical system: inline styles have the highest specificity, followed by ID selectors, then class/attribute/pseudo-class selectors, and finally element/pseudo-element selectors. This can be conceptualized as a four-part score (inline, ID, class, element). When creating multiple rules that may target the same elements, you can use the <kbd>color</kbd> property to set text colors, and specificity will determine which color is actually applied. Understanding specificity is crucial for predictable styling and debugging CSS conflicts. When two selectors have equal specificity, the one that comes last in the stylesheet wins.",
"task": "Examine the existing CSS rules and add a new rule with higher specificity to override the text color of the paragraph. Create a rule using '.content p' as the selector and set color: green.",
"previewHTML": "<div class=\"content\">\n <p>What color will this paragraph be? Look at the CSS rules and their specificity.</p>\n</div>",
"previewBaseCSS": "body { font-family: sans-serif; line-height: 1.5; padding: 20px; }",
"sandboxCSS": "p { border: 1px dashed gray; padding: 10px; }",
"codePrefix": "/* These CSS rules target the same paragraph but have different specificity */\n\n/* Rule 1: Element selector (lowest specificity) */\np {\n color: red;\n}\n\n/* Rule 2: Descendant selector (higher specificity than just 'p') */\n",
"initialCode": "",
"codeSuffix": "",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "^\\.content\\s+p\\s*{",
"message": "Use <kbd>.content p</kbd> as your selector (note the space between)",
"options": {
"caseSensitive": true
}
},
{
"type": "contains",
"value": "color:",
"message": "Include the <kbd>color</kbd> property"
},
{
"type": "contains",
"value": "green",
"message": ""
}
]
}

View File

@@ -1,17 +1,16 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "welcome",
"title": "Bienvenido",
"description": "Comienza con Code Crispies",
"mode": "css",
"title": "Code Crispies",
"description": "Welcome to Code Crispies - your interactive web development learning platform",
"mode": "html",
"difficulty": "beginner",
"excludeFromProgress": true,
"lessons": [
{
"id": "hello",
"title": "¡Hola!",
"description": "<strong>¡Bienvenido a Code Crispies!</strong> Aprende CSS y HTML con ejercicios prácticos.<br><br><strong>Cómo funciona:</strong><br>1. Lee la tarea a la izquierda<br>2. Escribe código en el editor<br>3. Ve los resultados en vivo en la vista previa<br><br><strong>Consejos:</strong> Usa <kbd>Ctrl+Z</kbd> para deshacer. Abre el menú (☰) para explorar todos los módulos.",
"task": "Escribe <code>Hello World</code> para comenzar",
"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>",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; text-align: center; font-size: 2rem; color: #6366f1; font-weight: bold; }",
"sandboxCSS": "",
@@ -22,9 +21,42 @@
{
"type": "contains",
"value": "Hello World",
"message": "Escribe <code>Hello World</code>"
"message": "Write <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/libretech/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",
"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": "",
"initialCode": "<p>Hello World! 🌍</p>\n<p>Hallo Welt!</p>\n<p>Bonjour le monde!</p>\n<p>¡Hola Mundo!</p>\n<p>Ciao Mondo!</p>\n<p>Olá Mundo!</p>\n<p>こんにちは世界!</p>\n<p>你好世界!</p>\n<p>안녕 세상!</p>\n<p>Привет мир!</p>\n<p dir=\"rtl\">שלום עולם!</p>\n<p dir=\"rtl\">مرحبا بالعالم!</p>",
"solution": "<p>Hello World! 🌍</p>\n<p>Hallo Welt!</p>\n<p>Bonjour le monde!</p>\n<p>¡Hola Mundo!</p>\n<p>Ciao Mondo!</p>\n<p>Olá Mundo!</p>\n<p>こんにちは世界!</p>\n<p>你好世界!</p>\n<p>안녕 세상!</p>\n<p>Привет мир!</p>\n<p dir=\"rtl\">שלום עולם!</p>\n<p dir=\"rtl\">مرحبا بالعالم!</p>",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "Hello World",
"message": "Click Next to continue"
}
]
},
{
"id": "playground",
"title": "Playground",
"mode": "playground",
"description": "",
"task": "",
"previewHTML": "",
"previewBaseCSS": "",
"sandboxCSS": "",
"initialCode": "<style>\n body {\n font-family: system-ui, sans-serif;\n padding: 20px;\n }\n</style>\n\n<h1>Hello World</h1>\n<p>Start coding!</p>",
"solution": "",
"previewContainer": "preview-area",
"validations": []
}
]
}

View File

@@ -2,18 +2,18 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "box-model",
"title": "CSS Box Model",
"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.",
"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.",
"difficulty": "beginner",
"lessons": [
{
"id": "box-model-1",
"title": "Padding",
"description": "Cada elemento en CSS es una caja con cuatro capas: contenido, padding, borde y margen. <strong>Padding</strong> crea espacio entre tu contenido y el borde de la caja.<br><br>Sin padding, el texto se aprieta incómodamente contra los bordes. El padding hace que el contenido sea legible y visualmente equilibrado.<br><br><pre>.card {\n padding: 1rem;\n}</pre>",
"task": "El texto dentro de esta tarjeta de perfil está pegado a los bordes. Dale algo de espacio interior para respirar.",
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"codePrefix": ".box {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "padding: 1rem;",
@@ -22,62 +22,62 @@
{
"type": "property_value",
"value": { "property": "padding", "expected": "1rem" },
"message": "¿Qué propiedad añade espacio entre el contenido y el borde del elemento?"
"message": "Set <kbd>padding: 1rem</kbd>"
}
]
},
{
"id": "box-model-2",
"title": "Borders",
"description": "Los bordes crean límites visuales alrededor de los elementos. El atajo <kbd>border</kbd> acepta tres valores: ancho, estilo y color.<br><br>Estilos comunes: <kbd>solid</kbd>, <kbd>dashed</kbd>, <kbd>dotted</kbd>, <kbd>none</kbd>",
"task": "Esta tarjeta necesita una línea de acento de color en su borde izquierdo.",
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"codePrefix": ".box {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "border-left: 4px solid steelblue;",
"solution": "border: 2px solid darkslategray;",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "border-left:\\s*4px\\s+solid\\s+steelblue",
"message": "Usa el atajo que define un borde en un solo lado",
"value": "border:\\s*2px\\s+solid\\s+darkslategray",
"message": "Set <kbd>border: 2px solid darkslategray</kbd>",
"options": { "caseSensitive": false }
}
]
},
{
"id": "box-model-3",
"title": "Margins",
"description": "Los márgenes crean espacio <em>fuera</em> del elemento, separándolo de sus vecinos. Mientras que el padding empuja el contenido hacia adentro, los márgenes empujan otros elementos hacia afuera.",
"task": "Estas dos tarjetas de perfil se están tocando. Añade algo de espacio debajo de cada tarjeta para separarlas.",
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article><article class=\"card\"><h3>Alex Rivera</h3><p>UX Designer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"codePrefix": ".outer {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "margin-bottom: 1rem;",
"solution": "margin: 1rem;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "margin-bottom", "expected": "1rem" },
"message": "¿Qué propiedad empuja los elementos vecinos hacia abajo?"
"value": { "property": "margin", "expected": "1rem" },
"message": "Set <kbd>margin: 1rem</kbd>"
}
]
},
{
"id": "box-model-4",
"title": "Box Sizing",
"description": "Por defecto, <kbd>width</kbd> solo establece el ancho del contenido. Padding y bordes se suman al total. Esto causa problemas de diseño.<br><br><kbd>box-sizing: border-box</kbd> incluye padding y borde en el ancho, haciendo el dimensionamiento predecible. La mayoría de desarrolladores aplican esto a todos los elementos.",
"task": "Ambas tarjetas tienen el mismo ancho, pero la izquierda se desborda porque el padding y el borde se suman encima. Corrige la tarjeta derecha para que su tamaño incluya el padding y el borde.",
"previewHTML": "<div class=\"demo\"><article class=\"card\">Content-box</article><article class=\"card fix\">Border-box</article></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .demo { display: flex; gap: 1rem; } .card { width: 200px; padding: 1rem; border: 4px solid steelblue; background: white; border-radius: 8px; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".fix {\n ",
"codePrefix": ".sized {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "box-sizing: border-box;",
@@ -86,104 +86,93 @@
{
"type": "property_value",
"value": { "property": "box-sizing", "expected": "border-box" },
"message": "¿Qué modo de tamaño incluye padding y borde en el ancho del elemento?"
"message": "Set <kbd>box-sizing: border-box</kbd>"
}
]
},
{
"id": "box-model-5",
"title": "Padding Shorthand",
"description": "Padding acepta 1-4 valores:<br>• 1 valor: todos los lados<br>• 2 valores: vertical | horizontal<br>• 4 valores: arriba | derecha | abajo | izquierda",
"task": "Este botón se siente muy apretado. Dale más espacio en los lados que arriba y abajo, usando el atajo de dos valores.",
"previewHTML": "<button class=\"btn\">Follow</button>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .btn { background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".btn {\n ",
"codePrefix": ".first {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "padding: 8px 1rem;",
"solution": "margin-bottom: 2rem;",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "padding:\\s*8px\\s+1rem",
"message": "Usa el atajo de dos valores: vertical primero, luego horizontal",
"options": { "caseSensitive": false }
"type": "property_value",
"value": { "property": "margin-bottom", "expected": "2rem" },
"message": "Set <kbd>margin-bottom: 2rem</kbd>"
}
]
},
{
"id": "box-model-6",
"title": "Margin Shorthand",
"description": "Margin usa el mismo patrón de atajo que padding. Un patrón común es centrar elementos de bloque horizontalmente con <kbd>margin: 0 auto</kbd>.",
"task": "Esta tarjeta está pegada a la izquierda. Céntrala horizontalmente usando el atajo de margen con márgenes laterales automáticos.",
"previewHTML": "<article class=\"card\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { width: 250px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; border-left: 4px solid steelblue; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".card {\n ",
"codePrefix": ".spaced {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "margin: 0 auto;",
"solution": "margin: 1rem 2rem;",
"previewContainer": "preview-area",
"validations": [
{
"type": "regex",
"value": "margin:\\s*0\\s+auto",
"message": "Usa el atajo que calcula márgenes horizontales iguales automáticamente",
"value": "margin:\\s*1rem\\s+2rem",
"message": "Set <kbd>margin: 1rem 2rem</kbd>",
"options": { "caseSensitive": false }
}
]
},
{
"id": "box-model-7",
"title": "Border Radius",
"description": "Aunque no es parte del modelo de caja clásico, <kbd>border-radius</kbd> redondea las esquinas de la caja de borde de un elemento. Usa <kbd>50%</kbd> en un elemento cuadrado para crear un círculo.",
"task": "La imagen cuadrada del avatar debería aparecer como un círculo perfecto.",
"previewHTML": "<article class=\"card\"><img class=\"avatar\" src=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='35' r='25' fill='%23666'/%3E%3Ccircle cx='50' cy='90' r='40' fill='%23666'/%3E%3C/svg%3E\" alt=\"Avatar\"><h3>Sarah Chen</h3><p>Frontend Developer</p></article>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1rem; text-align: center; } .avatar { width: 80px; height: 80px; background: #ddd; margin-bottom: 8px; } .card h3 { margin: 0 0 4px; } .card p { margin: 0; color: #666; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".avatar {\n ",
"codePrefix": ".padded {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "border-radius: 50%;",
"solution": "padding: 2rem;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "border-radius", "expected": "50%" },
"message": "¿Qué propiedad redondea las esquinas? Piensa en qué porcentaje crea un círculo"
"value": { "property": "padding", "expected": "2rem" },
"message": "Set <kbd>padding: 2rem</kbd>"
}
]
},
{
"id": "box-model-8",
"title": "Complete Card",
"description": "Combinemos todo. Esta tarjeta de notificación necesita estilo para verse profesional.",
"task": "Esta notificación necesita tres cosas: espacio interior para que el texto no esté apretado, un acento de color en el borde izquierdo y esquinas ligeramente redondeadas.",
"previewHTML": "<div class=\"alert\"><strong>New message</strong><p>You have 3 unread notifications</p></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .alert { background: seashell; } .alert strong { color: coral; } .alert p { margin: 4px 0 0; color: #666; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".alert {\n ",
"codePrefix": ".line {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "padding: 1rem;\n border-left: 4px solid coral;\n border-radius: 4px;",
"solution": "border-bottom: 4px solid dodgerblue;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "padding", "expected": "1rem" },
"message": "Añade espacio interior a la notificación"
},
{
"type": "regex",
"value": "border-left:\\s*4px\\s+solid\\s+coral",
"message": "Añade un acento de color en el borde izquierdo",
"value": "border-bottom:\\s*4px\\s+solid\\s+dodgerblue",
"message": "Set <kbd>border-bottom: 4px solid dodgerblue</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "border-radius", "expected": "4px" },
"message": "Suaviza las esquinas de la notificación"
}
]
}

View File

@@ -1,100 +1,115 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "units-variables",
"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.",
"title": "CSS Units & Variables",
"description": "Understand the variety of CSS measurement units and how to define and use custom properties for maintainable styles.",
"difficulty": "beginner",
"lessons": [
{
"id": "units-1",
"title": "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; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".article {\n ",
"codePrefix": "/* Set flexible sizing */\n.box {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "max-width: 40rem;",
"codeSuffix": "}",
"solution": " width: 80%;\n max-width: 37.5rem;",
"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": "40rem" },
"message": "Establece <kbd>max-width: 40rem</kbd>"
"value": { "property": "max-width", "expected": "37.5rem" },
"message": "Set max-width to <kbd>37.5rem</kbd>"
}
]
},
{
"id": "units-2",
"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; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ":root {\n ",
"codePrefix": "/* Define and use a CSS variable */\n:root {",
"initialCode": "",
"codeSuffix": "\n}\n\n.btn {\n background: var(--brand);\n}",
"solution": "--brand: steelblue;",
"codeSuffix": "}\n.themed { }",
"solution": " --main-color: #6200ee;\n}\n.themed {\n border-color: var(--main-color);",
"previewContainer": "preview-area",
"validations": [
{
"type": "contains",
"value": "--brand",
"message": "Define la variable <kbd>--brand</kbd>",
"value": "--main-color",
"message": "Define <kbd>--main-color</kbd> in :root",
"options": { "caseSensitive": false }
},
{
"type": "contains",
"value": "steelblue",
"message": "Establece el valor a <kbd>steelblue</kbd>",
"value": "var(--main-color)",
"message": "Use <kbd>var(--main-color)</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": "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; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".main {\n ",
"codePrefix": "/* Use calc for dynamic sizing */\n.sized {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "width: calc(100% - 200px);",
"codeSuffix": "}",
"solution": " width: calc(100% - 2rem);\n min-height: calc(10vh + 1rem);",
"previewContainer": "preview-area",
"validations": [
{ "type": "contains", "value": "calc", "message": "Use <kbd>calc()</kbd> function", "options": { "caseSensitive": false } },
{
"type": "regex",
"value": "width:\\s*calc\\(\\s*100%\\s*-\\s*200px\\s*\\)",
"message": "Establece <kbd>width: calc(100% - 200px)</kbd>",
"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>",
"options": { "caseSensitive": false }
}
]
},
{
"id": "units-4",
"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; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".hero {\n ",
"codePrefix": "/* Use viewport units */\n.view {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "min-height: 100vh;",
"codeSuffix": "}",
"solution": " width: 50vw;\n height: 20vh;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "min-height", "expected": "100vh" },
"message": "Establece <kbd>min-height: 100vh</kbd>"
}
{ "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>" }
]
}
]

View File

@@ -1,15 +1,15 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "transitions-animations",
"title": "Animaciones CSS",
"description": "Añade interactividad a tu UI mediante transiciones suaves de propiedades y animaciones basadas en keyframes.",
"title": "CSS Animations",
"description": "Bring interactivity to your UI by smoothly transitioning properties and creating keyframe-driven animations.",
"difficulty": "intermediate",
"lessons": [
{
"id": "transitions-1",
"title": "Transitions",
"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.",
"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.",
"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": "Usa la propiedad <kbd>transition</kbd>",
"message": "Use the <kbd>transition</kbd> property",
"options": { "caseSensitive": false }
},
{
"type": "regex",
"value": "transition:\\s*background-color\\s*0\\.3s",
"message": "Establece <kbd>transition: background-color 0.3s</kbd>",
"message": "Set <kbd>transition: background-color 0.3s</kbd>",
"options": { "caseSensitive": false }
}
]
@@ -36,8 +36,8 @@
{
"id": "transitions-2",
"title": "Timing Funcs",
"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>.",
"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>.",
"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": "Usa <kbd>transition-timing-function</kbd>",
"message": "Use <kbd>transition-timing-function</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "transition-timing-function", "expected": "ease-in-out" },
"message": "Establece timing a <kbd>ease-in-out</kbd>"
"message": "Set timing to <kbd>ease-in-out</kbd>"
}
]
},
{
"id": "transitions-3",
"title": "Keyframes",
"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>.",
"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>.",
"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": "En <kbd>50%</kbd>, usa <kbd>transform: translateY(-20px)</kbd>",
"message": "At <kbd>50%</kbd>, use <kbd>transform: translateY(-20px)</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "contains",
"value": "animation",
"message": "Usa la propiedad <kbd>animation</kbd> en <kbd>.ball</kbd>",
"message": "Use <kbd>animation</kbd> property on <kbd>.ball</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "regex",
"value": "animation:.*bounce.*1s.*infinite",
"message": "Aplica <kbd>animation: bounce 1s infinite</kbd>",
"message": "Apply <kbd>animation: bounce 1s infinite</kbd>",
"options": { "caseSensitive": false }
}
]
@@ -103,8 +103,8 @@
{
"id": "transitions-4",
"title": "Animation Properties",
"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>.",
"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>.",
"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": "Establece <kbd>animation-name: pulse</kbd>"
"message": "Set <kbd>animation-name: pulse</kbd>"
},
{
"type": "property_value",
"value": { "property": "animation-duration", "expected": "2s" },
"message": "Establece <kbd>animation-duration: 2s</kbd>"
"message": "Set <kbd>animation-duration: 2s</kbd>"
},
{
"type": "property_value",
"value": { "property": "animation-delay", "expected": "1s" },
"message": "Establece <kbd>animation-delay: 1s</kbd>"
"message": "Set <kbd>animation-delay: 1s</kbd>"
},
{
"type": "property_value",
"value": { "property": "animation-iteration-count", "expected": "2" },
"message": "Establece <kbd>animation-iteration-count: 2</kbd>"
"message": "Set <kbd>animation-iteration-count: 2</kbd>"
},
{
"type": "property_value",
"value": { "property": "animation-fill-mode", "expected": "forwards" },
"message": "Establece <kbd>animation-fill-mode: forwards</kbd>"
"message": "Set <kbd>animation-fill-mode: forwards</kbd>"
}
]
}

View File

@@ -2,14 +2,14 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "responsive-design",
"title": "CSS Responsive Design",
"description": "Adapta tus layouts a diferentes tamaños de pantalla usando media queries y técnicas de diseño fluido.",
"description": "Make your layouts adapt to different screen sizes using media queries and fluid design techniques.",
"difficulty": "intermediate",
"lessons": [
{
"id": "responsive-1",
"title": "Media Queries",
"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>.",
"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>.",
"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": "Usa <kbd>@media (max-width: 600px)</kbd>",
"message": "Use <kbd>@media (max-width: 600px)</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "contains",
"value": ".panel",
"message": "Selecciona <kbd>.panel</kbd> dentro de la media query",
"message": "Target <kbd>.panel</kbd> inside the media query",
"options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "background", "expected": "lightcoral" },
"message": "Establece <kbd>background: lightcoral</kbd>",
"message": "Set <kbd>background: lightcoral</kbd>",
"options": { "exact": false }
}
]
@@ -42,8 +42,8 @@
{
"id": "responsive-2",
"title": "Fluid Type",
"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.",
"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.",
"previewHTML": "<p class=\"text\">Fluid Typography</p>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; }",
"sandboxCSS": "",
@@ -53,50 +53,46 @@
"solution": " font-size: 5vw;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "font-size", "expected": "5vw" },
"message": "Establece <kbd>font-size: 5vw</kbd>"
}
{ "type": "property_value", "value": { "property": "font-size", "expected": "5vw" }, "message": "Set <kbd>font-size: 5vw</kbd>" }
]
},
{
"id": "responsive-3",
"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; }",
"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; }",
"sandboxCSS": "",
"codePrefix": ".features {\n ",
"codePrefix": "/* Create a responsive grid */\n.cards {",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 1rem;",
"codeSuffix": "}",
"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": "Establece <kbd>display: grid</kbd>"
"message": "Set <kbd>display: grid</kbd>"
},
{
"type": "regex",
"value": "repeat\\(auto-fit,\\s*minmax\\(200px,\\s*1fr\\)\\)",
"message": "Usa <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
"message": "Use <kbd>repeat(auto-fit, minmax(200px, 1fr))</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "gap", "expected": "1rem" },
"message": "Establece <kbd>gap: 1rem</kbd>"
"message": "Set <kbd>gap: 1rem</kbd>"
}
]
},
{
"id": "responsive-4",
"title": "Mobile-First",
"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>.",
"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>.",
"previewHTML": "<aside class=\"sidebar\">Sidebar</aside>",
"previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sidebar { background: #c8e6c9; padding: 1rem; }",
"sandboxCSS": "",
@@ -109,19 +105,19 @@
{
"type": "regex",
"value": "@media\\s*\\(min-width:\\s*768px\\)",
"message": "Usa <kbd>@media (min-width: 768px)</kbd>",
"message": "Use <kbd>@media (min-width: 768px)</kbd>",
"options": { "caseSensitive": false }
},
{
"type": "contains",
"value": ".sidebar",
"message": "Selecciona <kbd>.sidebar</kbd> en la media query",
"message": "Target <kbd>.sidebar</kbd> inside media query",
"options": { "caseSensitive": false }
},
{
"type": "property_value",
"value": { "property": "width", "expected": "250px" },
"message": "Establece <kbd>width: 250px</kbd>",
"message": "Set <kbd>width: 250px</kbd>",
"options": { "exact": false }
}
]

View File

@@ -2,65 +2,94 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-elements",
"title": "HTML Block & Inline",
"description": "Comprende la diferencia fundamental entre elementos contenedores (bloque) y elementos en línea",
"description": "Understanding the fundamental difference between container (block) and inline elements",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "block-vs-inline-intro",
"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>&lt;div&gt;</kbd>, <kbd>&lt;p&gt;</kbd>, <kbd>&lt;h1&gt;</kbd>, <kbd>&lt;section&gt;</kbd><br><br><strong>Elementos en línea</strong> fluyen dentro del texto y solo ocupan el ancho necesario. Ejemplos: <kbd>&lt;span&gt;</kbd>, <kbd>&lt;a&gt;</kbd>, <kbd>&lt;strong&gt;</kbd>, <kbd>&lt;em&gt;</kbd>",
"task": "Envuelve la palabra <kbd>importante</kbd> con etiquetas <kbd>&lt;strong&gt;</kbd> para ponerla en negrita. Observa cómo el párrafo (bloque) ocupa todo el ancho mientras que strong (en línea) fluye con el texto.",
"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>&lt;div&gt;</kbd>, <kbd>&lt;p&gt;</kbd>, <kbd>&lt;h1&gt;</kbd>, <kbd>&lt;section&gt;</kbd><br><br><strong>Inline elements</strong> flow within text and only take needed width. Examples: <kbd>&lt;span&gt;</kbd>, <kbd>&lt;a&gt;</kbd>, <kbd>&lt;strong&gt;</kbd>, <kbd>&lt;em&gt;</kbd>",
"task": "Wrap the word <kbd>important</kbd> with <kbd>&lt;strong&gt;</kbd> tags to make it bold. Notice how the paragraph (block) takes full width while strong (inline) flows with text.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 20px; } p { background: #e3f2fd; padding: 10px; } strong { background: #ffecb3; }",
"sandboxCSS": "",
"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>",
"initialCode": "<p>This is a paragraph with an important word.</p>",
"solution": "<p>This is a paragraph with an <strong>important</strong> word.</p>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "p",
"message": "Añade un elemento de párrafo <kbd>&lt;p&gt;</kbd>"
"message": "Add a <kbd>&lt;p&gt;</kbd> paragraph element"
},
{
"type": "parent_child",
"value": { "parent": "p", "child": "strong" },
"message": "Envuelve la palabra <kbd>importante</kbd> con etiquetas <kbd>&lt;strong&gt;</kbd>"
"message": "Wrap the word <kbd>important</kbd> with <kbd>&lt;strong&gt;</kbd> tags"
}
]
},
{
"id": "semantic-containers",
"title": "Etiquetas semánticas",
"description": "El HTML moderno usa contenedores semánticos que describen su contenido:<br><br><kbd>&lt;header&gt;</kbd> - Encabezado de página o sección<br><kbd>&lt;nav&gt;</kbd> - Enlaces de navegación<br><kbd>&lt;main&gt;</kbd> - Área de contenido principal<br><kbd>&lt;section&gt;</kbd> - Agrupación temática<br><kbd>&lt;article&gt;</kbd> - Contenido independiente<br><kbd>&lt;footer&gt;</kbd> - Pie de página o sección",
"task": "Crea una estructura básica de página:<br>1. Añade un <kbd>&lt;header&gt;</kbd> con un <kbd>&lt;h1&gt;</kbd> que contenga el texto <code>Mi Sitio Web</code><br>2. Añade un elemento <kbd>&lt;main&gt;</kbd> con un párrafo que diga <code>¡Bienvenido a mi sitio!</code><br>3. Añade un <kbd>&lt;footer&gt;</kbd> con un párrafo que diga <code>Copyright 2026</code>",
"title": "Semantic Tags",
"description": "Modern HTML uses semantic containers that describe their content:<br><br><kbd>&lt;header&gt;</kbd> - Page or section header<br><kbd>&lt;nav&gt;</kbd> - Navigation links<br><kbd>&lt;main&gt;</kbd> - Main content area<br><kbd>&lt;section&gt;</kbd> - Thematic grouping<br><kbd>&lt;article&gt;</kbd> - Self-contained content<br><kbd>&lt;footer&gt;</kbd> - Page or section footer",
"task": "Create a basic page structure:<br>1. Add a <kbd>&lt;header&gt;</kbd> with an <kbd>&lt;h1&gt;</kbd> containing the text <code>My Website</code><br>2. Add a <kbd>&lt;main&gt;</kbd> element with a paragraph saying <code>Welcome to my site!</code><br>3. Add a <kbd>&lt;footer&gt;</kbd> with a paragraph saying <code>Copyright 2026</code>",
"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>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>",
"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>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "header",
"message": "Añade un elemento <kbd>&lt;header&gt;</kbd>"
"message": "Add a <kbd>&lt;header&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "main",
"message": "Añade un elemento <kbd>&lt;main&gt;</kbd>"
"message": "Add a <kbd>&lt;main&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "footer",
"message": "Añade un elemento <kbd>&lt;footer&gt;</kbd>"
"message": "Add a <kbd>&lt;footer&gt;</kbd> element"
},
{
"type": "parent_child",
"value": { "parent": "header", "child": "h1" },
"message": "Añade un encabezado <kbd>&lt;h1&gt;</kbd> dentro de tu header"
"message": "Add an <kbd>&lt;h1&gt;</kbd> heading inside your header"
}
]
},
{
"id": "div-vs-span",
"title": "div & span",
"description": "When you need a container without semantic meaning:<br><br><kbd>&lt;div&gt;</kbd> - Generic block container (for layout/grouping)<br><kbd>&lt;span&gt;</kbd> - Generic inline container (for styling text portions)<br><br>Use semantic elements when possible, div/span when no semantic element fits.",
"task": "Wrap the word 'highlighted' in a <kbd>&lt;span&gt;</kbd> to style it differently. Wrap the whole quote in a <kbd>&lt;div&gt;</kbd>.",
"previewHTML": "",
"previewBaseCSS": "body { font-family: Georgia, serif; padding: 20px; } div { background: #f5f5f5; padding: 15px; border-left: 4px solid #1976d2; } span { background: #fff59d; padding: 2px 4px; }",
"sandboxCSS": "",
"initialCode": "The most highlighted moment was unforgettable.",
"solution": "<div>The most <span>highlighted</span> moment was unforgettable.</div>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "div",
"message": "Wrap everything in a <kbd>&lt;div&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "span",
"message": "Add a <kbd>&lt;span&gt;</kbd> around the word <kbd>highlighted</kbd>"
},
{
"type": "element_text",
"value": { "selector": "span", "text": "highlighted" },
"message": "The <kbd>&lt;span&gt;</kbd> should contain the word <kbd>highlighted</kbd>"
}
]
}

View File

@@ -1,100 +1,100 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-basic",
"title": "Formularios HTML",
"description": "Aprende a crear formularios con varios tipos de campos",
"title": "HTML Forms",
"description": "Learn to create forms with various input types",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "form-structure",
"title": "Estructura del formulario",
"description": "Todo formulario necesita un contenedor <kbd>&lt;form&gt;</kbd>. Dentro, usa <kbd>&lt;label&gt;</kbd> para describir campos y <kbd>&lt;input&gt;</kbd> para la entrada de datos.<br><br>El atributo <kbd>for</kbd> en los labels debe coincidir con el <kbd>id</kbd> de los inputs para accesibilidad.",
"task": "Crea un formulario con:<br>1. Un <kbd>&lt;label&gt;</kbd> con el texto <code>Nombre:</code> y el atributo <kbd>for=\"name\"</kbd><br>2. Un <kbd>&lt;input&gt;</kbd> de texto con los atributos <kbd>id=\"name\"</kbd> y <kbd>name=\"name\"</kbd>",
"title": "Form Structure",
"description": "Every form needs a <kbd>&lt;form&gt;</kbd> wrapper. Inside, use <kbd>&lt;label&gt;</kbd> to describe inputs and <kbd>&lt;input&gt;</kbd> for user data entry.<br><br>The <kbd>for</kbd> attribute on labels should match the <kbd>id</kbd> on inputs for accessibility.",
"task": "Create a form with:<br>1. A <kbd>&lt;label&gt;</kbd> with the text <code>Name:</code> and <kbd>for=\"name\"</kbd> attribute<br>2. A text <kbd>&lt;input&gt;</kbd> with <kbd>id=\"name\"</kbd> and <kbd>name=\"name\"</kbd> attributes",
"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\">Nombre:</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n</form>",
"solution": "<form>\n <label for=\"name\">Name:</label>\n <input type=\"text\" id=\"name\" name=\"name\">\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "form",
"message": "Envuelve todo en un elemento <kbd>&lt;form&gt;</kbd>"
"message": "Wrap everything in a <kbd>&lt;form&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "label",
"message": "Añade un <kbd>&lt;label&gt;</kbd> para tu campo"
"message": "Add a <kbd>&lt;label&gt;</kbd> for your input"
},
{
"type": "element_exists",
"value": "input",
"message": "Añade un elemento <kbd>&lt;input&gt;</kbd>"
"message": "Add an <kbd>&lt;input&gt;</kbd> element"
},
{
"type": "attribute_value",
"value": { "selector": "label", "attr": "for", "value": null },
"message": "Añade un atributo <kbd>for</kbd> a tu label"
"message": "Add a <kbd>for</kbd> attribute to your label"
},
{
"type": "attribute_value",
"value": { "selector": "input", "attr": "id", "value": null },
"message": "Añade un atributo <kbd>id</kbd> a tu campo"
"message": "Add an <kbd>id</kbd> attribute to your input"
}
]
},
{
"id": "input-types",
"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>&lt;label for=\"email\"&gt;Email:&lt;/label&gt;</kbd> y <kbd>&lt;input type=\"email\" id=\"email\"&gt;</kbd><br>2. Campo de contraseña: <kbd>&lt;label for=\"password\"&gt;Contraseña:&lt;/label&gt;</kbd> y <kbd>&lt;input type=\"password\" id=\"password\"&gt;</kbd>",
"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>&lt;label for=\"email\"&gt;Email:&lt;/label&gt;</kbd> and <kbd>&lt;input type=\"email\" id=\"email\"&gt;</kbd><br>2. A password field: <kbd>&lt;label for=\"password\"&gt;Password:&lt;/label&gt;</kbd> and <kbd>&lt;input type=\"password\" id=\"password\"&gt;</kbd>",
"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\">Contraseña:</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\">Password:</label>\n <input type=\"password\" id=\"password\" name=\"password\">\n</form>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "input[type='email']",
"message": "Añade un campo con type=\"email\""
"message": "Add an input with type=\"email\""
},
{
"type": "element_exists",
"value": "input[type='password']",
"message": "Añade un campo con type=\"password\""
"message": "Add an input with type=\"password\""
},
{
"type": "element_count",
"value": { "selector": "label", "min": 2 },
"message": "Añade labels para ambos campos"
"message": "Add labels for both inputs"
}
]
},
{
"id": "submit-button",
"title": "Botón de envío",
"description": "Los formularios necesitan una forma de enviar datos. Usa:<br><br><kbd>&lt;button type=\"submit\"&gt;</kbd> - Preferido, contenido flexible<br><kbd>&lt;input type=\"submit\"&gt;</kbd> - Botón de solo texto<br><br>El texto del botón debe estar orientado a la acción (ej. <code>Iniciar Sesión</code>, 'Registrar', 'Enviar').",
"task": "Añade un botón de envío al formulario con el texto <code>Iniciar Sesión</code>.",
"title": "Submit Button",
"description": "Forms need a way to submit data. Use:<br><br><kbd>&lt;button type=\"submit\"&gt;</kbd> - Preferred, flexible content<br><kbd>&lt;input type=\"submit\"&gt;</kbd> - Simple text-only button<br><br>The button text should be action-oriented (e.g., <code>Sign In</code>, 'Register', 'Send').",
"task": "Add a submit button to the form with the text <code>Sign In</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\">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>",
"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>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "button[type='submit'], input[type='submit']",
"message": "Añade un botón de envío a tu formulario"
"message": "Add a submit button to your form"
},
{
"type": "element_text",
"value": { "selector": "button", "text": "Iniciar Sesión" },
"message": "El botón debe decir <kbd>Iniciar Sesión</kbd>"
"value": { "selector": "button", "text": "Sign In" },
"message": "The button should say <kbd>Sign In</kbd>"
}
]
}

View File

@@ -1,32 +1,110 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-validation",
"title": "Validación de formularios",
"description": "Usa la validación integrada de HTML5 para mejor experiencia de usuario",
"title": "HTML Validation",
"description": "Learn HTML5 built-in form validation attributes",
"mode": "html",
"difficulty": "beginner",
"difficulty": "intermediate",
"lessons": [
{
"id": "required-fields",
"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>&lt;input type=\"text\" required&gt;</kbd>",
"task": "Haz que ambos campos (nombre y email) sean requeridos añadiendo el atributo <kbd>required</kbd> a cada campo.",
"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>&lt;input type=\"text\" required&gt;</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.",
"previewHTML": "",
"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; }",
"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; }",
"sandboxCSS": "",
"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>",
"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>",
"previewContainer": "preview-area",
"validations": [
{
"type": "attribute_value",
"value": { "selector": "input[name='name']", "attr": "required", "value": true },
"message": "Añade <kbd>required</kbd> al campo de nombre"
"message": "Add the <kbd>required</kbd> attribute to the name input"
},
{
"type": "attribute_value",
"value": { "selector": "input[name='email']", "attr": "required", "value": true },
"message": "Añade <kbd>required</kbd> al campo de email"
"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>"
}
]
}

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-details-summary",
"title": "HTML Details & Summary",
"description": "Crea secciones expandibles sin JavaScript",
"description": "Create expandable content sections without JavaScript",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "details-summary-basic",
"title": "Primer widget",
"description": "El elemento <kbd>&lt;details&gt;</kbd> crea una sección plegable. El <kbd>&lt;summary&gt;</kbd> proporciona la etiqueta clickeable.<br><br>¡Haz clic en el resumen para mostrar el contenido oculto - sin JavaScript!",
"task": "Crea un elemento <kbd>&lt;details&gt;</kbd> con:<br>1. Un <kbd>&lt;summary&gt;</kbd> que diga <code>Click to reveal</code><br>2. Un <kbd>&lt;p&gt;</kbd> con el texto <code>This content was hidden!</code>",
"title": "First Widget",
"description": "The <kbd>&lt;details&gt;</kbd> element creates a collapsible section. The <kbd>&lt;summary&gt;</kbd> provides the clickable label.<br><br>Click the summary to toggle the hidden content - no JavaScript needed!",
"task": "Create a <kbd>&lt;details&gt;</kbd> element with:<br>1. A <kbd>&lt;summary&gt;</kbd> saying <code>Click to reveal</code><br>2. A <kbd>&lt;p&gt;</kbd> with the text <code>This content was hidden!</code>",
"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": "Añade un elemento <kbd>&lt;details&gt;</kbd>"
"message": "Add a <kbd>&lt;details&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "summary",
"message": "Añade un <kbd>&lt;summary&gt;</kbd> dentro del details"
"message": "Add a <kbd>&lt;summary&gt;</kbd> inside the details"
},
{
"type": "parent_child",
"value": { "parent": "details", "child": "summary" },
"message": "El <kbd>&lt;summary&gt;</kbd> debe estar dentro de <kbd>&lt;details&gt;</kbd>"
"message": "The <kbd>&lt;summary&gt;</kbd> must be inside <kbd>&lt;details&gt;</kbd>"
},
{
"type": "parent_child",
"value": { "parent": "details", "child": "p" },
"message": "Añade un <kbd>&lt;p&gt;</kbd> dentro de <kbd>&lt;details&gt;</kbd> para el contenido oculto"
"message": "Add a <kbd>&lt;p&gt;</kbd> inside <kbd>&lt;details&gt;</kbd> for the hidden content"
}
]
},
{
"id": "details-open-attribute",
"title": "Expandido por defecto",
"description": "Por defecto, <kbd>&lt;details&gt;</kbd> está cerrado. Añade el atributo <kbd>open</kbd> para mostrar el contenido inicialmente.<br><br>Este es un atributo booleano - solo añade <kbd>open</kbd> sin valor.",
"task": "Añade el atributo <kbd>open</kbd> al elemento <kbd>&lt;details&gt;</kbd> para mostrar el contenido por defecto.",
"title": "Pre-expanded Details",
"description": "By default, <kbd>&lt;details&gt;</kbd> is closed. Add the <kbd>open</kbd> attribute to show the content initially.<br><br>This is a boolean attribute - just add <kbd>open</kbd> without a value.",
"task": "Add the <kbd>open</kbd> attribute to the <kbd>&lt;details&gt;</kbd> element to show the content by default.",
"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": "Añade el atributo <kbd>open</kbd> a <kbd>&lt;details&gt;</kbd>"
"message": "Add the <kbd>open</kbd> attribute to <kbd>&lt;details&gt;</kbd>"
}
]
},
{
"id": "faq-accordion",
"title": "Acordeón FAQ",
"description": "Múltiples elementos <kbd>&lt;details&gt;</kbd> crean un FAQ estilo acordeón. Cada pregunta puede expandirse independientemente.<br><br><b>Pro tip:</b> Escribe <kbd>details*3&gt;summary+p</kbd> y presiona Tab para expansión Emmet. <kbd>*3</kbd> crea 3 elementos, <kbd>&gt;</kbd> anida dentro, <kbd>+</kbd> añade hermanos.",
"task": "Crea una sección FAQ con:<br>1. Un <kbd>&lt;h1&gt;</kbd> que diga <code>Frequently Asked Questions</code><br>2. Tres elementos <kbd>&lt;details&gt;</kbd>, cada uno con una pregunta en <kbd>&lt;summary&gt;</kbd> y una respuesta en <kbd>&lt;p&gt;</kbd>",
"title": "FAQ Accordion",
"description": "Multiple <kbd>&lt;details&gt;</kbd> elements create an accordion-style FAQ. Each question can be expanded independently.<br><br><b>Pro tip:</b> Type <kbd>details*3&gt;summary+p</kbd> and press Tab for Emmet expansion. <kbd>*3</kbd> creates 3 elements, <kbd>&gt;</kbd> nests inside, <kbd>+</kbd> adds siblings.",
"task": "Create an FAQ section with:<br>1. An <kbd>&lt;h1&gt;</kbd> saying <code>Frequently Asked Questions</code><br>2. Three <kbd>&lt;details&gt;</kbd> elements, each with a question in <kbd>&lt;summary&gt;</kbd> and an answer in <kbd>&lt;p&gt;</kbd>",
"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": "Añade un encabezado <kbd>&lt;h1&gt;</kbd> para el título del FAQ"
"message": "Add an <kbd>&lt;h1&gt;</kbd> heading for the FAQ title"
},
{
"type": "element_count",
"value": { "selector": "details", "min": 3 },
"message": "Crea al menos 3 elementos <kbd>&lt;details&gt;</kbd> para el FAQ"
"message": "Create at least 3 <kbd>&lt;details&gt;</kbd> elements for the FAQ"
},
{
"type": "element_count",
"value": { "selector": "summary", "min": 3 },
"message": "Cada <kbd>&lt;details&gt;</kbd> necesita un <kbd>&lt;summary&gt;</kbd> para la pregunta"
"message": "Each <kbd>&lt;details&gt;</kbd> needs a <kbd>&lt;summary&gt;</kbd> for the question"
},
{
"type": "element_count",
"value": { "selector": "details p", "min": 3 },
"message": "Cada <kbd>&lt;details&gt;</kbd> necesita un <kbd>&lt;p&gt;</kbd> para la respuesta"
"message": "Each <kbd>&lt;details&gt;</kbd> needs a <kbd>&lt;p&gt;</kbd> for the answer"
}
]
}

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-progress-meter",
"title": "HTML Progress & Meter",
"description": "Muestra el estado de completado y mediciones escalares nativamente",
"description": "Display completion status and scalar measurements natively",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "progress-basic",
"title": "Barras de progreso",
"description": "El elemento <kbd>&lt;progress&gt;</kbd> muestra el progreso de una tarea. Usa <kbd>value</kbd> para el progreso actual y <kbd>max</kbd> para el total.<br><br><b>Nota:</b> ¡Esto no es una etiqueta de cierre automático! Escribe <kbd>&lt;progress&gt;...&lt;/progress&gt;</kbd> con texto alternativo dentro para navegadores antiguos.",
"task": "Crea una barra de progreso mostrando 70% de completado:<br>1. Añade un <kbd>&lt;label&gt;</kbd> que diga <code>Download:</code><br>2. Añade un <kbd>&lt;progress&gt;</kbd> con <kbd>value=\"70\"</kbd> y <kbd>max=\"100\"</kbd>",
"title": "Progress Bars",
"description": "The <kbd>&lt;progress&gt;</kbd> element shows task completion. Use <kbd>value</kbd> for current progress and <kbd>max</kbd> for the total.<br><br><b>Note:</b> This is not a self-closing tag! Write <kbd>&lt;progress&gt;...&lt;/progress&gt;</kbd> with fallback text inside for older browsers.",
"task": "Create a progress bar showing 70% completion:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Download:</code><br>2. Add a <kbd>&lt;progress&gt;</kbd> with <kbd>value=\"70\"</kbd> and <kbd>max=\"100\"</kbd>",
"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": "Añade un elemento <kbd>&lt;progress&gt;</kbd>"
"message": "Add a <kbd>&lt;progress&gt;</kbd> element"
},
{
"type": "attribute_value",
"value": { "selector": "progress", "attr": "value", "value": "70" },
"message": "Establece <kbd>value=</kbd>\"70\" en el elemento progress"
"message": "Set <kbd>value=</kbd>\"70\" on the progress element"
},
{
"type": "attribute_value",
"value": { "selector": "progress", "attr": "max", "value": "100" },
"message": "Establece <kbd>max=</kbd>\"100\" en el elemento progress"
"message": "Set <kbd>max=</kbd>\"100\" on the progress element"
},
{
"type": "element_exists",
"value": "label",
"message": "Añade un <kbd>&lt;label&gt;</kbd> para la barra de progreso"
"message": "Add a <kbd>&lt;label&gt;</kbd> for the progress bar"
}
]
},
{
"id": "progress-indeterminate",
"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>&lt;p&gt;</kbd> que diga <code>Loading...</code><br>2. Añade un <kbd>&lt;progress&gt;</kbd> sin atributo value",
"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>&lt;p&gt;</kbd> saying <code>Loading...</code><br>2. Add a <kbd>&lt;progress&gt;</kbd> without a value attribute",
"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": "Añade un elemento <kbd>&lt;progress&gt;</kbd>"
"message": "Add a <kbd>&lt;progress&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "p",
"message": "Añade un <kbd>&lt;p&gt;</kbd> con texto de carga"
"message": "Add a <kbd>&lt;p&gt;</kbd> with loading text"
}
]
},
{
"id": "meter-gauge",
"title": "Indicadores meter",
"description": "El elemento <kbd>&lt;meter&gt;</kbd> muestra un valor escalar dentro de un rango. Úsalo para mediciones como espacio en disco, batería o calificaciones.<br><br>Establece <kbd>low</kbd>, <kbd>high</kbd> y <kbd>optimum</kbd> para definir rangos buenos/malos - ¡el navegador lo colorea correspondientemente!",
"task": "Crea un indicador de nivel de batería:<br>1. Añade un <kbd>&lt;label&gt;</kbd> que diga <code>Battery:</code><br>2. Añade un <kbd>&lt;meter&gt;</kbd> con:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> y <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> y <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>",
"title": "Meter Gauges",
"description": "The <kbd>&lt;meter&gt;</kbd> element displays a scalar value within a range. Use it for measurements like disk space, battery, or ratings.<br><br>Set <kbd>low</kbd>, <kbd>high</kbd>, and <kbd>optimum</kbd> to define good/bad ranges - the browser colors it accordingly!",
"task": "Create a battery level meter:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Battery:</code><br>2. Add a <kbd>&lt;meter&gt;</kbd> with:<br> - <kbd>value=\"0.8\"</kbd><br> - <kbd>min=\"0\"</kbd> and <kbd>max=\"1\"</kbd><br> - <kbd>low=\"0.2\"</kbd> and <kbd>high=\"0.8\"</kbd><br> - <kbd>optimum=\"1\"</kbd>",
"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,42 +79,22 @@
{
"type": "element_exists",
"value": "meter",
"message": "Añade un elemento <kbd>&lt;meter&gt;</kbd>"
"message": "Add a <kbd>&lt;meter&gt;</kbd> element"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "value", "value": "0.8" },
"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"
"message": "Set <kbd>value=</kbd>\"0.8\" on the meter"
},
{
"type": "attribute_value",
"value": { "selector": "meter", "attr": "low", "value": "0.2" },
"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"
"message": "Set <kbd>low=</kbd>\"0.2\" to define the low threshold"
},
{
"type": "element_exists",
"value": "label",
"message": "Añade un <kbd>&lt;label&gt;</kbd> para el meter"
"message": "Add a <kbd>&lt;label&gt;</kbd> for the meter"
}
]
}

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-datalist",
"title": "Datalist",
"description": "Proporciona sugerencias para campos de texto sin JavaScript",
"description": "Provide suggestions for text inputs without JavaScript",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "datalist-basic",
"title": "Campo con sugerencias",
"description": "El elemento <kbd>&lt;datalist&gt;</kbd> proporciona sugerencias de autocompletado para campos. Conéctalo usando el atributo <kbd>list</kbd> en el input que coincida con el <kbd>id</kbd> del datalist.<br><br>Los usuarios pueden escribir libremente - ¡las sugerencias son solo ayudas!",
"task": "Crea un selector de navegador:<br>1. Añade un <kbd>&lt;label&gt;</kbd> que diga <code>Navegador:</code><br>2. Añade un <kbd>&lt;input&gt;</kbd> con <kbd>list=\"browsers\"</kbd><br>3. Añade un <kbd>&lt;datalist id=\"browsers\"&gt;</kbd> con opciones para Chrome, Firefox y Safari",
"title": "Input with Suggestions",
"description": "The <kbd>&lt;datalist&gt;</kbd> element provides autocomplete suggestions for inputs. Connect it using the <kbd>list</kbd> attribute on the input matching the datalist's <kbd>id</kbd>.<br><br>Users can still type freely - suggestions are just helpers!",
"task": "Create a browser selector:<br>1. Add a <kbd>&lt;label&gt;</kbd> saying <code>Browser:</code><br>2. Add an <kbd>&lt;input&gt;</kbd> with <kbd>list=\"browsers\"</kbd><br>3. Add a <kbd>&lt;datalist id=\"browsers\"&gt;</kbd> with options for Chrome, Firefox, and Safari",
"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": "Añade un elemento <kbd>&lt;datalist&gt;</kbd>"
"message": "Add a <kbd>&lt;datalist&gt;</kbd> element"
},
{
"type": "attribute_value",
"value": { "selector": "input", "attr": "list", "value": "browsers" },
"message": "Conecta el input al datalist usando <kbd>list=</kbd>\"browsers\""
"message": "Connect the input to datalist using <kbd>list=</kbd>\"browsers\""
},
{
"type": "element_count",
"value": { "selector": "option", "min": 3 },
"message": "Añade al menos 3 elementos <kbd>&lt;option&gt;</kbd> dentro de <kbd>&lt;datalist&gt;</kbd>"
"message": "Add at least 3 <kbd>&lt;option&gt;</kbd> elements inside <kbd>&lt;datalist&gt;</kbd>"
},
{
"type": "element_exists",
"value": "label",
"message": "Añade un <kbd>&lt;label&gt;</kbd> para el campo"
"message": "Add a <kbd>&lt;label&gt;</kbd> for the input"
}
]
},
{
"id": "datalist-countries",
"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>&lt;label&gt;</kbd> que diga <code>País:</code><br>2. Añade un <kbd>&lt;input&gt;</kbd> con <kbd>list=\"countries\"</kbd><br>3. Añade un <kbd>&lt;datalist id=\"countries\"&gt;</kbd> con al menos 4 opciones de países",
"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>&lt;label&gt;</kbd> saying <code>Country:</code><br>2. Add an <kbd>&lt;input&gt;</kbd> with <kbd>list=\"countries\"</kbd><br>3. Add a <kbd>&lt;datalist id=\"countries\"&gt;</kbd> with at least 4 country options",
"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": "Añade un elemento <kbd>&lt;datalist&gt;</kbd>"
"message": "Add a <kbd>&lt;datalist&gt;</kbd> element"
},
{
"type": "attribute_value",
"value": { "selector": "datalist", "attr": "id", "value": "countries" },
"message": "Establece <kbd>id=</kbd>\"countries\" en el datalist"
"message": "Set <kbd>id=</kbd>\"countries\" on the datalist"
},
{
"type": "attribute_value",
"value": { "selector": "input", "attr": "list", "value": "countries" },
"message": "Conecta el input usando <kbd>list=</kbd>\"countries\""
"message": "Connect the input using <kbd>list=</kbd>\"countries\""
},
{
"type": "element_count",
"value": { "selector": "option", "min": 4 },
"message": "Añade al menos 4 opciones de países"
"message": "Add at least 4 country options"
}
]
}

View File

@@ -1,16 +1,16 @@
{
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-dialog",
"title": "Diálogos",
"description": "Crea diálogos modales sin bibliotecas JavaScript",
"title": "Dialogs",
"description": "Create modal dialogs without JavaScript libraries",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "dialog-basic",
"title": "Abrir diálogo",
"description": "El elemento <kbd>&lt;dialog&gt;</kbd> crea un modal nativo. Añade el atributo <kbd>open</kbd> para mostrarlo.<br><br>¡Usa <kbd>&lt;form method=\"dialog\"&gt;</kbd> dentro para cerrarlo al enviar el formulario - sin JavaScript!",
"task": "Crea un diálogo con:<br>1. El atributo <kbd>open</kbd> para mostrarlo<br>2. Un <kbd>&lt;h2&gt;</kbd> que diga <code>¡Bienvenido!</code><br>3. Un <kbd>&lt;p&gt;</kbd> con un mensaje de saludo<br>4. Un <kbd>&lt;form method=\"dialog\"&gt;</kbd> con un botón de cerrar",
"title": "Open Dialog",
"description": "The <kbd>&lt;dialog&gt;</kbd> element creates a native modal. Add the <kbd>open</kbd> attribute to show it.<br><br>Use <kbd>&lt;form method=\"dialog\"&gt;</kbd> inside to close it when the form submits - no JavaScript needed!",
"task": "Create a dialog with:<br>1. The <kbd>open</kbd> attribute to show it<br>2. An <kbd>&lt;h2&gt;</kbd> saying <code>Welcome!</code><br>3. A <kbd>&lt;p&gt;</kbd> with a greeting message<br>4. A <kbd>&lt;form method=\"dialog\"&gt;</kbd> with a close button",
"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": "Añade un elemento <kbd>&lt;dialog&gt;</kbd>"
"message": "Add a <kbd>&lt;dialog&gt;</kbd> element"
},
{
"type": "attribute_value",
"value": { "selector": "dialog", "attr": "open", "value": true },
"message": "Añade el atributo <kbd>open</kbd> para mostrar el diálogo"
"message": "Add the <kbd>open</kbd> attribute to show the dialog"
},
{
"type": "element_exists",
"value": "dialog h2",
"message": "Añade un encabezado <kbd>&lt;h2&gt;</kbd> dentro del diálogo"
"message": "Add an <kbd>&lt;h2&gt;</kbd> heading inside the dialog"
},
{
"type": "element_exists",
"value": "form[method='dialog']",
"message": "Añade un <kbd>&lt;form method=\"dialog\"&gt;</kbd> para cerrar"
"message": "Add a <kbd>&lt;form method=\"dialog\"&gt;</kbd> for closing"
},
{
"type": "element_exists",
"value": "dialog button",
"message": "Añade un botón de cerrar dentro del formulario"
"message": "Add a close button inside the form"
}
]
},
{
"id": "dialog-form",
"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>&lt;h2&gt;</kbd> que diga <code>Confirmar eliminación</code><br>3. Un <kbd>&lt;p&gt;</kbd> preguntando <code>¿Estás seguro?</code><br>4. Un <kbd>&lt;form method=\"dialog\"&gt;</kbd> con botones Cancelar y Eliminar",
"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>&lt;h2&gt;</kbd> saying <code>Confirm Delete</code><br>3. A <kbd>&lt;p&gt;</kbd> asking <code>Are you sure?</code><br>4. A <kbd>&lt;form method=\"dialog\"&gt;</kbd> with Cancel and Delete buttons",
"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": "Añade un <kbd>&lt;dialog&gt;</kbd> con el atributo open"
"message": "Add a <kbd>&lt;dialog&gt;</kbd> with the open attribute"
},
{
"type": "element_exists",
"value": "dialog h2",
"message": "Añade un encabezado al diálogo"
"message": "Add a heading to the dialog"
},
{
"type": "element_exists",
"value": "form[method='dialog']",
"message": "Añade un <kbd>&lt;form method=\"dialog\"&gt;</kbd> para los botones"
"message": "Add a <kbd>&lt;form method=\"dialog\"&gt;</kbd> for the buttons"
},
{
"type": "element_count",
"value": { "selector": "dialog button", "min": 2 },
"message": "Añade al menos 2 botones (Cancelar y Confirmar)"
"message": "Add at least 2 buttons (Cancel and Confirm)"
}
]
}

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-forms-fieldset",
"title": "Fieldsets",
"description": "Agrupa controles de formulario con elementos fieldset y legend",
"description": "Group form controls with fieldset and legend elements",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "fieldset-basic",
"title": "Agrupar con Fieldset",
"description": "El elemento <kbd>&lt;fieldset&gt;</kbd> agrupa controles de formulario relacionados. Añade un <kbd>&lt;legend&gt;</kbd> como primer hijo para dar un título al grupo.<br><br>Esto ayuda con la accesibilidad y organización visual de formularios complejos.",
"task": "Crea un formulario con un fieldset:<br>1. Un elemento <kbd>&lt;form&gt;</kbd><br>2. Un <kbd>&lt;fieldset&gt;</kbd> dentro<br>3. Un <kbd>&lt;legend&gt;</kbd> que diga <code>Información Personal</code><br>4. Dos campos etiquetados para nombre y email",
"title": "Grouping with Fieldset",
"description": "The <kbd>&lt;fieldset&gt;</kbd> element groups related form controls together. Add a <kbd>&lt;legend&gt;</kbd> as the first child to give the group a title.<br><br>This helps with accessibility and visual organization of complex forms.",
"task": "Create a form with a fieldset:<br>1. A <kbd>&lt;form&gt;</kbd> element<br>2. A <kbd>&lt;fieldset&gt;</kbd> inside<br>3. A <kbd>&lt;legend&gt;</kbd> saying <code>Personal Info</code><br>4. Two labeled inputs for name and email",
"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": "Añade un elemento <kbd>&lt;form&gt;</kbd>"
"message": "Add a <kbd>&lt;form&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "fieldset",
"message": "Añade un <kbd>&lt;fieldset&gt;</kbd> dentro del formulario"
"message": "Add a <kbd>&lt;fieldset&gt;</kbd> inside the form"
},
{
"type": "element_exists",
"value": "legend",
"message": "Añade un <kbd>&lt;legend&gt;</kbd> para titular tu fieldset"
"message": "Add a <kbd>&lt;legend&gt;</kbd> to title your fieldset"
},
{
"type": "element_count",
"value": { "selector": "label", "min": 2 },
"message": "Añade al menos 2 etiquetas"
"message": "Add at least 2 labels"
},
{
"type": "element_count",
"value": { "selector": "input", "min": 2 },
"message": "Añade al menos 2 campos de entrada"
"message": "Add at least 2 input fields"
}
]
},
{
"id": "fieldset-textarea",
"title": "Añadiendo Textarea",
"description": "El elemento <kbd>&lt;textarea&gt;</kbd> crea una entrada de texto multilínea, perfecta para contenido largo como mensajes o descripciones.<br><br>Usa los atributos <kbd>rows</kbd> y <kbd>cols</kbd> para establecer el tamaño predeterminado.",
"task": "Crea un formulario de contacto:<br>1. Un <kbd>&lt;fieldset&gt;</kbd> con <kbd>&lt;legend&gt;</kbd> <code>Contáctanos</code><br>2. Un <kbd>&lt;input&gt;</kbd> etiquetado para email<br>3. Un <kbd>&lt;textarea&gt;</kbd> etiquetado para el mensaje<br>4. Un <kbd>&lt;button&gt;</kbd> de envío",
"title": "Adding Textarea",
"description": "The <kbd>&lt;textarea&gt;</kbd> element creates a multi-line text input, perfect for longer content like messages or descriptions.<br><br>Use <kbd>rows</kbd> and <kbd>cols</kbd> attributes to set default size.",
"task": "Create a contact form:<br>1. A <kbd>&lt;fieldset&gt;</kbd> with <kbd>&lt;legend&gt;</kbd> <code>Contact Us</code><br>2. A labeled <kbd>&lt;input&gt;</kbd> for email<br>3. A labeled <kbd>&lt;textarea&gt;</kbd> for the message<br>4. A submit <kbd>&lt;button&gt;</kbd>",
"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": "Añade un elemento <kbd>&lt;fieldset&gt;</kbd>"
"message": "Add a <kbd>&lt;fieldset&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "legend",
"message": "Añade un elemento <kbd>&lt;legend&gt;</kbd>"
"message": "Add a <kbd>&lt;legend&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "textarea",
"message": "Añade un <kbd>&lt;textarea&gt;</kbd> para el mensaje"
"message": "Add a <kbd>&lt;textarea&gt;</kbd> for the message"
},
{
"type": "element_exists",
"value": "button",
"message": "Añade un botón de envío"
"message": "Add a submit button"
},
{
"type": "element_exists",
"value": "input",
"message": "Añade un campo de entrada para el email"
"message": "Add an input field for email"
}
]
},
{
"id": "fieldset-multiple",
"title": "Múltiples Fieldsets",
"description": "Los formularios complejos pueden usar múltiples elementos <kbd>&lt;fieldset&gt;</kbd> para organizar diferentes secciones.<br><br>Esto mejora la usabilidad en formularios largos como registro o checkout.",
"task": "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",
"title": "Multiple Fieldsets",
"description": "Complex forms can use multiple <kbd>&lt;fieldset&gt;</kbd> elements to organize different sections.<br><br>This improves usability for long forms like registration or checkout pages.",
"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",
"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": "Crea al menos 2 fieldsets"
"message": "Create at least 2 fieldsets"
},
{
"type": "element_count",
"value": { "selector": "legend", "min": 2 },
"message": "Añade un legend a cada fieldset"
"message": "Add a legend to each fieldset"
},
{
"type": "element_exists",
"value": "textarea",
"message": "Añade un textarea para la bio"
"message": "Add a textarea for the bio"
},
{
"type": "element_exists",
"value": "button",
"message": "Añade un botón de envío"
"message": "Add a submit button"
},
{
"type": "element_count",
"value": { "selector": "input", "min": 2 },
"message": "Añade al menos 2 campos de entrada"
"message": "Add at least 2 input fields"
}
]
}

View File

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

View File

@@ -2,15 +2,15 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-marquee",
"title": "HTML Marquee",
"description": "Crea texto desplazable con el clásico (¡obsoleto pero divertido!) elemento marquee",
"description": "Create scrolling text with the classic (deprecated but fun!) marquee element",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "marquee-basic",
"title": "Texto Desplazable",
"description": "El elemento <kbd>&lt;marquee&gt;</kbd> crea texto desplazable - ¡un clásico de la web temprana! Aunque está obsoleto, todavía funciona en la mayoría de navegadores.<br><br>Nota: Para producción, usa animaciones CSS. ¡Pero para aprender y divertirse, marquee es genial!",
"task": "Crea un marquee simple:<br>1. Añade un elemento <kbd>&lt;marquee&gt;</kbd><br>2. Pon texto dentro como <code>¡Bienvenido a mi sitio web!</code>",
"title": "Scrolling Text",
"description": "The <kbd>&lt;marquee&gt;</kbd> element creates scrolling text - a classic from the early web! While deprecated, it still works in most browsers.<br><br>Note: For production, use CSS animations instead. But for learning and fun, marquee is great!",
"task": "Create a simple marquee:<br>1. Add a <kbd>&lt;marquee&gt;</kbd> element<br>2. Put some text inside like <code>Welcome to my website!</code>",
"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": "Añade un elemento <kbd>&lt;marquee&gt;</kbd>"
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element"
}
]
},
{
"id": "marquee-direction",
"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>&lt;marquee&gt;</kbd><br>2. Pon <kbd>behavior=\"alternate\"</kbd> para hacerlo rebotar<br>3. Añade texto divertido",
"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>&lt;marquee&gt;</kbd> element<br>2. Set <kbd>behavior=\"alternate\"</kbd> to make it bounce<br>3. Add some fun text",
"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": "Añade un elemento <kbd>&lt;marquee&gt;</kbd>"
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element"
},
{
"type": "attribute_value",
"value": { "selector": "marquee", "attr": "behavior", "value": "alternate" },
"message": "Añade <kbd>behavior=</kbd>\"alternate\" para hacerlo rebotar"
"message": "Add <kbd>behavior=</kbd>\"alternate\" to make it bounce"
}
]
},
{
"id": "marquee-retro",
"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>&lt;marquee&gt;</kbd> con <kbd>direction=\"left\"</kbd><br>2. Pon <kbd>scrollamount=\"5\"</kbd> para desplazamiento suave<br>3. Añade un titular de última hora dentro",
"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>&lt;marquee&gt;</kbd> with <kbd>direction=\"left\"</kbd><br>2. Set <kbd>scrollamount=\"5\"</kbd> for smooth scrolling<br>3. Add a breaking news headline inside",
"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": "Añade un elemento <kbd>&lt;marquee&gt;</kbd>"
"message": "Add a <kbd>&lt;marquee&gt;</kbd> element"
},
{
"type": "attribute_value",
"value": { "selector": "marquee", "attr": "direction", "value": "left" },
"message": "Añade <kbd>direction=</kbd>\"left\" para desplazamiento horizontal"
"message": "Add <kbd>direction=</kbd>\"left\" for horizontal scrolling"
},
{
"type": "attribute_value",
"value": { "selector": "marquee", "attr": "scrollamount", "value": "5" },
"message": "Añade <kbd>scrollamount=</kbd>\"5\" para velocidad suave"
"message": "Add <kbd>scrollamount=</kbd>\"5\" for smooth speed"
}
]
}

View File

@@ -2,169 +2,99 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "html-svg",
"title": "HTML SVG",
"description": "Dibuja gráficos vectoriales escalables directamente en HTML",
"description": "Draw scalable vector graphics directly in HTML",
"mode": "html",
"difficulty": "beginner",
"lessons": [
{
"id": "svg-circle",
"title": "Dibujando círculos",
"description": "SVG (Scalable Vector Graphics) permite dibujar formas directamente en HTML. El elemento <kbd>&lt;svg&gt;</kbd> es el contenedor con atributos <kbd>width</kbd> y <kbd>height</kbd>.<br><br>Usa <kbd>&lt;circle&gt;</kbd> con <kbd>cx</kbd>, <kbd>cy</kbd> (centro) y <kbd>r</kbd> (radio) para dibujar círculos.",
"task": "Crea un SVG con un círculo:<br>1. Un <kbd>&lt;svg&gt;</kbd> con width=\"200\" y height=\"200\"<br>2. Un <kbd>&lt;circle&gt;</kbd> centrado en (100,100) con radio 50<br>3. Añade un color <kbd>fill</kbd>",
"title": "Drawing Circles",
"description": "SVG (Scalable Vector Graphics) lets you draw shapes directly in HTML. The <kbd>&lt;svg&gt;</kbd> element is the container, with <kbd>width</kbd> and <kbd>height</kbd> attributes.<br><br>Use <kbd>&lt;circle&gt;</kbd> with <kbd>cx</kbd>, <kbd>cy</kbd> (center) and <kbd>r</kbd> (radius) to draw circles.",
"task": "Create an SVG with a circle:<br>1. An <kbd>&lt;svg&gt;</kbd> with width=\"200\" and height=\"200\"<br>2. A <kbd>&lt;circle&gt;</kbd> centered at (100,100) with radius 50<br>3. Add a <kbd>fill</kbd> color",
"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=\"steelblue\" />\n</svg>",
"solution": "<svg width=\"200\" height=\"200\">\n <circle cx=\"100\" cy=\"100\" r=\"50\" fill=\"#3498db\" />\n</svg>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "svg",
"message": "Añade un elemento <kbd>&lt;svg&gt;</kbd>"
"message": "Add an <kbd>&lt;svg&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "circle",
"message": "Añade un elemento <kbd>&lt;circle&gt;</kbd> dentro del SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "width", "value": "200" },
"message": "Establece <kbd>width=</kbd>\"200\" en el SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "height", "value": "200" },
"message": "Establece <kbd>height=</kbd>\"200\" en el SVG"
"message": "Add a <kbd>&lt;circle&gt;</kbd> element inside the SVG"
},
{
"type": "attribute_value",
"value": { "selector": "circle", "attr": "cx", "value": "100" },
"message": "Establece <kbd>cx=</kbd>\"100\" para el centro horizontal del círculo"
"message": "Set <kbd>cx=</kbd>\"100\" for the circle's horizontal center"
},
{
"type": "attribute_value",
"value": { "selector": "circle", "attr": "cy", "value": "100" },
"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"
"message": "Set <kbd>cy=</kbd>\"100\" for the circle's vertical center"
}
]
},
{
"id": "svg-rect-line",
"title": "Rectángulos y líneas",
"description": "Dibuja rectángulos con <kbd>&lt;rect&gt;</kbd> usando <kbd>x</kbd>, <kbd>y</kbd>, <kbd>width</kbd>, <kbd>height</kbd>.<br><br>Dibuja líneas con <kbd>&lt;line&gt;</kbd> usando <kbd>x1</kbd>, <kbd>y1</kbd> (inicio) y <kbd>x2</kbd>, <kbd>y2</kbd> (fin). ¡Las líneas necesitan un color <kbd>stroke</kbd>!",
"task": "Crea un SVG con:<br>1. Un <kbd>&lt;svg&gt;</kbd> (200x150)<br>2. Un <kbd>&lt;rect&gt;</kbd> en posición (20,20) con tamaño 80x60<br>3. Una <kbd>&lt;line&gt;</kbd> de (120,30) a (180,90) con color stroke",
"title": "Rectangles & Lines",
"description": "Draw rectangles with <kbd>&lt;rect&gt;</kbd> using <kbd>x</kbd>, <kbd>y</kbd>, <kbd>width</kbd>, <kbd>height</kbd>.<br><br>Draw lines with <kbd>&lt;line&gt;</kbd> using <kbd>x1</kbd>, <kbd>y1</kbd> (start) and <kbd>x2</kbd>, <kbd>y2</kbd> (end). Lines need a <kbd>stroke</kbd> color!",
"task": "Create an SVG with:<br>1. An <kbd>&lt;svg&gt;</kbd> (200x150)<br>2. A <kbd>&lt;rect&gt;</kbd> at position (20,20) with size 80x60<br>3. A <kbd>&lt;line&gt;</kbd> from (120,30) to (180,90) with a stroke color",
"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=\"tomato\" />\n <line x1=\"120\" y1=\"30\" x2=\"180\" y2=\"90\" stroke=\"slategray\" stroke-width=\"3\" />\n</svg>",
"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>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "svg",
"message": "Añade un elemento <kbd>&lt;svg&gt;</kbd>"
"message": "Add an <kbd>&lt;svg&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "rect",
"message": "Añade un elemento <kbd>&lt;rect&gt;</kbd>"
"message": "Add a <kbd>&lt;rect&gt;</kbd> element"
},
{
"type": "element_exists",
"value": "line",
"message": "Añade un elemento <kbd>&lt;line&gt;</kbd>"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "width", "value": "200" },
"message": "Establece <kbd>width=</kbd>\"200\" en el SVG"
},
{
"type": "attribute_value",
"value": { "selector": "svg", "attr": "height", "value": "150" },
"message": "Establece <kbd>height=</kbd>\"150\" en el SVG"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "x", "value": "20" },
"message": "Establece <kbd>x=</kbd>\"20\" en el rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "y", "value": "20" },
"message": "Establece <kbd>y=</kbd>\"20\" en el rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "width", "value": "80" },
"message": "Establece <kbd>width=</kbd>\"80\" en el rect"
},
{
"type": "attribute_value",
"value": { "selector": "rect", "attr": "height", "value": "60" },
"message": "Establece <kbd>height=</kbd>\"60\" en el rect"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "x1", "value": "120" },
"message": "Establece <kbd>x1=</kbd>\"120\" en la line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "y1", "value": "30" },
"message": "Establece <kbd>y1=</kbd>\"30\" en la line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "x2", "value": "180" },
"message": "Establece <kbd>x2=</kbd>\"180\" en la line"
},
{
"type": "attribute_value",
"value": { "selector": "line", "attr": "y2", "value": "90" },
"message": "Establece <kbd>y2=</kbd>\"90\" en la line"
},
{
"type": "contains",
"value": "stroke",
"message": "Añade un color <kbd>stroke</kbd> a la line"
"message": "Add a <kbd>&lt;line&gt;</kbd> element"
}
]
},
{
"id": "svg-shapes",
"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>&lt;svg&gt;</kbd> (200x200)<br>2. Un <kbd>&lt;circle&gt;</kbd> grande para la cara<br>3. Dos <kbd>&lt;circle&gt;</kbd> pequeños para los ojos<br>4. Una <kbd>&lt;line&gt;</kbd> para la sonrisa",
"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>&lt;svg&gt;</kbd> (200x200)<br>2. A large <kbd>&lt;circle&gt;</kbd> for the face<br>3. Two small <kbd>&lt;circle&gt;</kbd> elements for eyes<br>4. A <kbd>&lt;line&gt;</kbd> for the smile",
"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=\"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>",
"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>",
"previewContainer": "preview-area",
"validations": [
{
"type": "element_exists",
"value": "svg",
"message": "Añade un elemento <kbd>&lt;svg&gt;</kbd>"
"message": "Add an <kbd>&lt;svg&gt;</kbd> element"
},
{
"type": "element_count",
"value": { "selector": "circle", "min": 3 },
"message": "Añade al menos 3 círculos (1 cara + 2 ojos)"
"message": "Add at least 3 circles (1 face + 2 eyes)"
},
{
"type": "element_exists",
"value": "line",
"message": "Añade una <kbd>&lt;line&gt;</kbd> para la sonrisa"
"message": "Add a <kbd>&lt;line&gt;</kbd> for the smile"
}
]
}

View File

@@ -2,18 +2,18 @@
"$schema": "../../schemas/code-crispies-module-schema.json",
"id": "flexbox",
"title": "CSS Flexbox",
"description": "Domina el modelo de caja flexible para diseños responsivos modernos",
"description": "Master the flexible box layout model for modern responsive designs",
"difficulty": "intermediate",
"lessons": [
{
"id": "flexbox-1",
"title": "Container",
"description": "Antes de flexbox, incluso los diseños simples requerían floats, hacks de posicionamiento o diseños basados en tablas. Flexbox (Flexible Box Layout) revolucionó CSS al proporcionar un sistema de diseño unidimensional diseñado específicamente para distribuir espacio y alinear contenido.<br><br><strong>Cómo funciona:</strong> Cuando estableces <kbd>display: flex</kbd> en un elemento, se convierte en un <em>contenedor flex</em>. Sus hijos directos automáticamente se convierten en <em>elementos flex</em> que fluyen a lo largo de un eje principal (horizontal por defecto). Esta única propiedad transforma elementos de bloque apilados en una fila horizontal.<br><br><strong>Los dos ejes:</strong><br>• <em>Eje principal</em> La dirección primaria del flujo de elementos (row = izquierda→derecha)<br>• <em>Eje transversal</em> Perpendicular al principal (row = arriba→abajo)<br><br><pre>.nav {\n display: flex;\n}</pre>",
"task": "Este menú de navegación se apila verticalmente. Añade <kbd>display: flex</kbd> para organizar los enlaces horizontalmente.",
"previewHTML": "<nav class=\"nav\"><a href=\"#\">Home</a><a href=\"#\">Products</a><a href=\"#\">About</a><a href=\"#\">Contact</a></nav>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .nav { background: #1a1a2e; padding: 1rem; } .nav a { color: white; text-decoration: none; padding: 8px 1rem; border-radius: 4px; } .nav a:hover { background: rgba(255,255,255,0.1); }",
"sandboxCSS": "",
"codePrefix": ".nav {\n ",
"description": "Learn how to create a flex container and understand the main and cross axes.",
"task": "Add <kbd>display: flex</kbd> to <kbd>.wrap</kbd> to create a flexbox layout.",
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; }",
"codePrefix": ".wrap {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "display: flex;",
@@ -21,41 +21,61 @@
"validations": [
{
"type": "property_value",
"value": { "property": "display", "expected": "flex" },
"message": "Establece <kbd>display: flex</kbd>"
"value": {
"property": "display",
"expected": "flex"
},
"message": "Set <kbd>display: flex</kbd>"
}
]
},
{
"id": "flexbox-2",
"title": "Gap",
"description": "La propiedad <kbd>gap</kbd> añade espaciado consistente entre elementos flex sin necesidad de márgenes. Solo crea espacio entre elementos, no en los bordes.",
"task": "Añade <kbd>gap: 1rem</kbd> para espaciar uniformemente los enlaces de navegación.",
"previewHTML": "<nav class=\"nav\"><a href=\"#\">Home</a><a href=\"#\">Products</a><a href=\"#\">About</a><a href=\"#\">Contact</a></nav>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .nav { background: #1a1a2e; padding: 1rem; display: flex; } .nav a { color: white; text-decoration: none; padding: 8px 1rem; border-radius: 4px; background: rgba(255,255,255,0.1); }",
"sandboxCSS": "",
"codePrefix": ".nav {\n ",
"title": "Direction & Wrap",
"description": "Control the direction and wrapping of flex items within a container.",
"task": "Add <kbd>flex-direction: column</kbd> and <kbd>flex-wrap: wrap</kbd> to <kbd>.wrap</kbd>.",
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div><div class='box'>4</div><div class='box'>5</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; height: 16rem; display: flex; }",
"codePrefix": ".wrap {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "gap: 1rem;",
"solution": "flex-direction: column;\n flex-wrap: wrap;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "gap", "expected": "1rem" },
"message": "Establece <kbd>gap: 1rem</kbd>"
"value": {
"property": "flex-direction",
"expected": "column"
},
"message": "Set <kbd>flex-direction: column</kbd>",
"options": {
"exact": true
}
},
{
"type": "property_value",
"value": {
"property": "flex-wrap",
"expected": "wrap"
},
"message": "Set <kbd>flex-wrap: wrap</kbd>",
"options": {
"exact": true
}
}
]
},
{
"id": "flexbox-3",
"title": "Justify Content",
"description": "<kbd>justify-content</kbd> distribuye elementos a lo largo del eje principal. Valores comunes:<br>• <kbd>flex-start</kbd> agrupar al inicio<br>• <kbd>flex-end</kbd> agrupar al final<br>• <kbd>center</kbd> centrar elementos<br>• <kbd>space-between</kbd> espacio igual entre elementos<br>• <kbd>space-around</kbd> espacio igual alrededor de elementos",
"task": "Empuja el botón \"Login\" hacia la derecha estableciendo <kbd>justify-content: space-between</kbd> en la navegación.",
"previewHTML": "<nav class=\"nav\"><div class=\"links\"><a href=\"#\">Home</a><a href=\"#\">Products</a><a href=\"#\">About</a></div><a href=\"#\" class=\"login\">Login</a></nav>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .nav { background: #1a1a2e; padding: 1rem; display: flex; } .links { display: flex; gap: 8px; } .nav a { color: white; text-decoration: none; padding: 8px 1rem; border-radius: 4px; } .nav a:hover { background: rgba(255,255,255,0.1); } .login { background: steelblue; }",
"sandboxCSS": "",
"codePrefix": ".nav {\n ",
"description": "Learn how to align flex items along the main axis of the flex container.",
"task": "Add <kbd>justify-content: space-between</kbd> to <kbd>.wrap</kbd> to distribute the boxes evenly.",
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; }",
"codePrefix": ".wrap {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "justify-content: space-between;",
@@ -63,20 +83,26 @@
"validations": [
{
"type": "property_value",
"value": { "property": "justify-content", "expected": "space-between" },
"message": "Establece <kbd>justify-content: space-between</kbd>"
"value": {
"property": "justify-content",
"expected": "space-between"
},
"message": "Set <kbd>justify-content: space-between</kbd>",
"options": {
"exact": true
}
}
]
},
{
"id": "flexbox-4",
"title": "Align Items",
"description": "<kbd>align-items</kbd> controla la alineación en el eje transversal (vertical cuando flex-direction es row). Los valores incluyen:<br>• <kbd>stretch</kbd> estirar para llenar (por defecto)<br>• <kbd>flex-start</kbd> alinear arriba<br>• <kbd>flex-end</kbd> alinear abajo<br>• <kbd>center</kbd> centrar verticalmente",
"task": "El logo y los enlaces de navegación tienen diferentes alturas. Céntralos verticalmente con <kbd>align-items: center</kbd>.",
"previewHTML": "<header class=\"header\"><div class=\"logo\">ACME</div><nav><a href=\"#\">Products</a><a href=\"#\">Pricing</a><a href=\"#\">Docs</a></nav></header>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .header { background: white; padding: 1rem 2rem; display: flex; justify-content: space-between; border-bottom: 1px solid #eee; } .logo { font-size: 1.5rem; font-weight: bold; color: steelblue; } nav { display: flex; gap: 1rem; } nav a { color: #333; text-decoration: none; font-size: 0.9rem; }",
"sandboxCSS": "",
"codePrefix": ".header {\n ",
"description": "Control how flex items are aligned along the cross axis of the flex container.",
"task": "Add <kbd>align-items: center</kbd> to <kbd>.wrap</kbd> to vertically center the boxes.",
"previewHTML": "<div class='wrap'><div class='box tall'>1</div><div class='box'>2</div><div class='box short'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; display: flex; justify-content: center; } .tall { height: 6rem; } .short { height: 3rem; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; height: 10rem; }",
"codePrefix": ".wrap {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "align-items: center;",
@@ -84,50 +110,62 @@
"validations": [
{
"type": "property_value",
"value": { "property": "align-items", "expected": "center" },
"message": "Establece <kbd>align-items: center</kbd>"
"value": {
"property": "align-items",
"expected": "center"
},
"message": "Set <kbd>align-items: center</kbd>",
"options": {
"exact": true
}
}
]
},
{
"id": "flexbox-5",
"title": "Flex Wrap",
"description": "Por defecto, los elementos flex se comprimen en una línea. <kbd>flex-wrap: wrap</kbd> permite que los elementos fluyan a múltiples líneas cuando se quedan sin espacio.",
"task": "Estas tarjetas desbordan el contenedor. Añade <kbd>flex-wrap: wrap</kbd> para permitir que pasen a nuevas filas.",
"previewHTML": "<div class=\"cards\"><article class=\"card\">Card 1</article><article class=\"card\">Card 2</article><article class=\"card\">Card 3</article><article class=\"card\">Card 4</article><article class=\"card\">Card 5</article><article class=\"card\">Card 6</article></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .cards { display: flex; gap: 1rem; } .card { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); min-width: 120px; text-align: center; }",
"sandboxCSS": "",
"codePrefix": ".cards {\n ",
"title": "Flex Grow",
"description": "The <kbd>flex</kbd> property controls how much an item grows relative to others.",
"task": "Add <kbd>flex: 2</kbd> to <kbd>.box2</kbd> to make it grow twice as wide.",
"previewHTML": "<div class='wrap'><div class='box box1'>1</div><div class='box box2'>2</div><div class='box box3'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; display: flex; align-items: center; justify-content: center; } .box1 { background: coral; flex: 1; } .box2 { background: mediumseagreen; } .box3 { background: gold; flex: 1; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; }",
"codePrefix": ".box2 {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "flex-wrap: wrap;",
"solution": "flex: 2;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "flex-wrap", "expected": "wrap" },
"message": "Establece <kbd>flex-wrap: wrap</kbd>"
"value": {
"property": "flex",
"expected": "2"
},
"message": "Set <kbd>flex: 2</kbd>"
}
]
},
{
"id": "flexbox-6",
"title": "Flex Grow",
"description": "La propiedad <kbd>flex</kbd> en los elementos controla cómo crecen y se encogen. <kbd>flex: 1</kbd> hace que un elemento crezca para llenar el espacio disponible. Múltiples elementos con <kbd>flex: 1</kbd> comparten el espacio equitativamente.",
"task": "Haz que el campo de búsqueda se expanda para llenar el espacio disponible estableciendo <kbd>flex: 1</kbd> en <kbd>.search</kbd>.",
"previewHTML": "<div class=\"toolbar\"><input class=\"search\" type=\"text\" placeholder=\"Search...\"><button class=\"btn\">Search</button><button class=\"btn\">Filters</button></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .toolbar { display: flex; gap: 8px; padding: 1rem; background: #f5f5f5; border-radius: 8px; } .search { padding: 8px 1rem; border: 1px solid #ddd; border-radius: 4px; font-size: 1rem; } .btn { padding: 8px 1rem; background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; }",
"sandboxCSS": "",
"codePrefix": ".search {\n ",
"title": "Align Self",
"description": "Use <kbd>align-self</kbd> to override alignment for a single flex item.",
"task": "Add <kbd>align-self: flex-start</kbd> to <kbd>.middle</kbd> to move it to the top.",
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box middle'>2</div><div class='box'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; } .middle { background: mediumseagreen; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; height: 12rem; align-items: center; }",
"codePrefix": ".middle {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "flex: 1;",
"solution": "align-self: flex-start;",
"previewContainer": "preview-area",
"validations": [
{
"type": "property_value",
"value": { "property": "flex", "expected": "1" },
"message": "Establece <kbd>flex: 1</kbd>"
"value": {
"property": "align-self",
"expected": "flex-start"
},
"message": "Set <kbd>align-self: flex-start</kbd>"
}
]
}

View File

@@ -8,126 +8,194 @@
{
"id": "flexbox-1",
"title": "Container",
"description": "Before flexbox, creating even simple layouts required floats, positioning hacks, or table-based layouts. Flexbox (Flexible Box Layout) revolutionized CSS by providing a one-dimensional layout system designed specifically for distributing space and aligning content.<br><br><strong>How it works:</strong> When you set <kbd>display: flex</kbd> on an element, it becomes a <em>flex container</em>. Its direct children automatically become <em>flex items</em> that flow along a main axis (horizontal by default). This single property transforms stacked block elements into a horizontal row.<br><br><strong>The two axes:</strong><br>• <em>Main axis</em> The primary direction items flow (row = left→right)<br>• <em>Cross axis</em> Perpendicular to main (row = top→bottom)<br><br><pre>.nav {\n display: flex;\n}</pre>",
"task": "The navigation links are stacking vertically. Make them display side by side in a horizontal row.",
"previewHTML": "<nav class=\"nav\"><a href=\"#\">Home</a><a href=\"#\">Products</a><a href=\"#\">About</a><a href=\"#\">Contact</a></nav>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .nav { background: #1a1a2e; padding: 1rem; } .nav a { color: white; text-decoration: none; padding: 8px 1rem; border-radius: 4px; } .nav a:hover { background: rgba(255,255,255,0.1); }",
"sandboxCSS": "",
"codePrefix": ".nav {\n ",
"description": "Learn how to create a flex container and understand the main and cross axes.<br><br><pre>.container {\n display: flex;\n justify-content: center;\n align-items: center;\n}</pre>",
"task": "Add <kbd>display: flex</kbd> to <kbd>.wrap</kbd> to create a flexbox layout.",
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; }",
"codePrefix": ".wrap {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "display: flex;",
"previewContainer": "preview-area",
"concept": {
"explanation": "Setting display: flex creates a flex container, which establishes a new flex formatting context for its direct children. By default, this creates a horizontal main axis (left to right) and a vertical cross axis (top to bottom). All direct children automatically become flex items that can be controlled by flex properties.",
"diagram": "┌─────────────────────────────────┐\n│ FLEX CONTAINER (.wrap) │\n│ │\n│ Main Axis (horizontal) → │\n│ ┌───┐ ┌───┐ ┌───┐ │\n│ │ 1 │ │ 2 │ │ 3 │ ← Items │\n│ └───┘ └───┘ └───┘ │\n│ ↑ │\n│ Cross Axis (vertical) │\n└─────────────────────────────────┘",
"containerVsItem": "display: flex is a CONTAINER property applied to the parent element. It affects how the container lays out its children, but doesn't change the children themselves."
},
"validations": [
{
"type": "property_value",
"value": { "property": "display", "expected": "flex" },
"message": "Try changing the display mode to create a flex container"
"value": {
"property": "display",
"expected": "flex"
},
"message": "Set <kbd>display: flex</kbd>"
}
]
},
{
"id": "flexbox-2",
"title": "Gap",
"description": "The <kbd>gap</kbd> property adds consistent spacing between flex items without needing margins. It only creates space between items, not around the edges.",
"task": "The navigation links are crammed together with no breathing room. Add 1rem of spacing between them.",
"previewHTML": "<nav class=\"nav\"><a href=\"#\">Home</a><a href=\"#\">Products</a><a href=\"#\">About</a><a href=\"#\">Contact</a></nav>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .nav { background: #1a1a2e; padding: 1rem; display: flex; } .nav a { color: white; text-decoration: none; padding: 8px 1rem; border-radius: 4px; background: rgba(255,255,255,0.1); }",
"sandboxCSS": "",
"codePrefix": ".nav {\n ",
"title": "Direction & Wrap",
"description": "Control the direction and wrapping of flex items within a container.",
"task": "Add <kbd>flex-direction: column</kbd> and <kbd>flex-wrap: wrap</kbd> to <kbd>.wrap</kbd>.",
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div><div class='box'>4</div><div class='box'>5</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; height: 16rem; display: flex; }",
"codePrefix": ".wrap {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "gap: 1rem;",
"solution": "flex-direction: column;\n flex-wrap: wrap;",
"previewContainer": "preview-area",
"concept": {
"explanation": "flex-direction changes which axis is the main axis: row (default) flows horizontally, while column flows vertically. This swaps how justify-content and align-items work. flex-wrap allows items to wrap onto new lines when they don't fit, instead of shrinking or overflowing.",
"diagram": "flex-direction: column\n\n┌──────────────┐\n│ Container │\n│ │\n│ ┌──┐ ┌──┐ │ Main Axis\n│ │1 │ │4 │ │ ↓\n│ └──┘ └──┘ │ (vertical)\n│ ┌──┐ ┌──┐ │\n│ │2 │ │5 │ │\n│ └──┘ └──┘ │ ← Cross Axis\n│ ┌──┐ │ (horizontal)\n│ │3 │ │\n│ └──┘ │\n└──────────────┘",
"containerVsItem": "Both flex-direction and flex-wrap are CONTAINER properties. They control how the container arranges its children, not the children themselves."
},
"validations": [
{
"type": "property_value",
"value": { "property": "gap", "expected": "1rem" },
"message": "Use the property that adds spacing between flex items"
"value": {
"property": "flex-direction",
"expected": "column"
},
"message": "Set <kbd>flex-direction: column</kbd>",
"options": {
"exact": true
}
},
{
"type": "property_value",
"value": {
"property": "flex-wrap",
"expected": "wrap"
},
"message": "Set <kbd>flex-wrap: wrap</kbd>",
"options": {
"exact": true
}
}
]
},
{
"id": "flexbox-3",
"title": "Justify Content",
"description": "<kbd>justify-content</kbd> distributes items along the main axis. Common values:<br>• <kbd>flex-start</kbd> pack items at the start<br>• <kbd>flex-end</kbd> pack at the end<br>• <kbd>center</kbd> center items<br>• <kbd>space-between</kbd> equal space between items<br>• <kbd>space-around</kbd> equal space around items",
"task": "The Login button should sit on the far right, with the other links staying on the left. Distribute the space between them.",
"previewHTML": "<nav class=\"nav\"><div class=\"links\"><a href=\"#\">Home</a><a href=\"#\">Products</a><a href=\"#\">About</a></div><a href=\"#\" class=\"login\">Login</a></nav>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .nav { background: #1a1a2e; padding: 1rem; display: flex; } .links { display: flex; gap: 8px; } .nav a { color: white; text-decoration: none; padding: 8px 1rem; border-radius: 4px; } .nav a:hover { background: rgba(255,255,255,0.1); } .login { background: steelblue; }",
"sandboxCSS": "",
"codePrefix": ".nav {\n ",
"description": "Learn how to align flex items along the main axis of the flex container.",
"task": "Add <kbd>justify-content: space-between</kbd> to <kbd>.wrap</kbd> to distribute the boxes evenly.",
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box'>2</div><div class='box'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; }",
"codePrefix": ".wrap {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "justify-content: space-between;",
"previewContainer": "preview-area",
"concept": {
"explanation": "justify-content controls how flex items are distributed along the main axis (horizontal by default). space-between places the first item at the start, the last at the end, and distributes remaining items with equal spacing between them. Other values include flex-start, center, flex-end, and space-around.",
"diagram": "justify-content: space-between\n\n┌─────────────────────────────┐\n│ ┌───┐ ┌───┐ ┌───┐ │\n│ │ 1 │ │ 2 │ │ 3 │ │\n│ └───┘ └───┘ └───┘ │\n│ ↑ ↑ ↑ │\n│ start equal gap end │\n│◄──────────────────────────► │\n│ Main Axis │\n└─────────────────────────────┘",
"containerVsItem": "justify-content is a CONTAINER property. The parent controls how its children are spaced, not the children themselves."
},
"validations": [
{
"type": "property_value",
"value": { "property": "justify-content", "expected": "space-between" },
"message": "Use the property that distributes items along the main axis"
"value": {
"property": "justify-content",
"expected": "space-between"
},
"message": "Set <kbd>justify-content: space-between</kbd>",
"options": {
"exact": true
}
}
]
},
{
"id": "flexbox-4",
"title": "Align Items",
"description": "<kbd>align-items</kbd> controls alignment on the cross axis (vertical when flex-direction is row). Values include:<br>• <kbd>stretch</kbd> stretch to fill (default)<br>• <kbd>flex-start</kbd> align to top<br>• <kbd>flex-end</kbd> align to bottom<br>• <kbd>center</kbd> center vertically",
"task": "The logo and nav links sit at different heights. Center them vertically so they line up.",
"previewHTML": "<header class=\"header\"><div class=\"logo\">ACME</div><nav><a href=\"#\">Products</a><a href=\"#\">Pricing</a><a href=\"#\">Docs</a></nav></header>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; margin: 0; } .header { background: white; padding: 1rem 2rem; display: flex; justify-content: space-between; border-bottom: 1px solid #eee; } .logo { font-size: 1.5rem; font-weight: bold; color: steelblue; } nav { display: flex; gap: 1rem; } nav a { color: #333; text-decoration: none; font-size: 0.9rem; }",
"sandboxCSS": "",
"codePrefix": ".header {\n ",
"description": "Control how flex items are aligned along the cross axis of the flex container.",
"task": "Add <kbd>align-items: center</kbd> to <kbd>.wrap</kbd> to vertically center the boxes.",
"previewHTML": "<div class='wrap'><div class='box tall'>1</div><div class='box'>2</div><div class='box short'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; display: flex; justify-content: center; } .tall { height: 6rem; } .short { height: 3rem; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; height: 10rem; }",
"codePrefix": ".wrap {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "align-items: center;",
"previewContainer": "preview-area",
"concept": {
"explanation": "align-items controls how flex items are aligned along the cross axis (vertical by default). While justify-content handles spacing along the main axis, align-items handles alignment perpendicular to it. The center value aligns all items to the middle of the cross axis, regardless of their individual heights.",
"diagram": "align-items: center\n\n┌──────────────────────┐ ↑\n│ │ │\n│ ┌────┐ │ │ Cross\n│ │ 1 │ ┌──┐ │ │ Axis\n│ │ │ │2 │ ┌─┐ │ │\n│ ────┼────┼──┼──┼─┼─┼─│ center line\n│ │ │ └──┘ └─┘ │ │\n│ └────┘ 3 │ │\n│ │ ↓\n└──────────────────────┘",
"containerVsItem": "align-items is a CONTAINER property that sets the default cross-axis alignment for all child items. Individual items can override this with align-self."
},
"validations": [
{
"type": "property_value",
"value": { "property": "align-items", "expected": "center" },
"message": "Use the property that controls cross-axis alignment"
"value": {
"property": "align-items",
"expected": "center"
},
"message": "Set <kbd>align-items: center</kbd>",
"options": {
"exact": true
}
}
]
},
{
"id": "flexbox-5",
"title": "Flex Wrap",
"description": "By default, flex items squeeze onto one line. <kbd>flex-wrap: wrap</kbd> allows items to flow onto multiple lines when they run out of space.",
"task": "The cards overflow the container instead of fitting within it. Allow the items to flow onto new rows when they run out of space.",
"previewHTML": "<div class=\"cards\"><article class=\"card\">Card 1</article><article class=\"card\">Card 2</article><article class=\"card\">Card 3</article><article class=\"card\">Card 4</article><article class=\"card\">Card 5</article><article class=\"card\">Card 6</article></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; background: #f5f5f5; } .cards { display: flex; gap: 1rem; } .card { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); min-width: 120px; text-align: center; }",
"sandboxCSS": "",
"codePrefix": ".cards {\n ",
"title": "Flex Grow",
"description": "The <kbd>flex</kbd> property controls how much an item grows relative to others.",
"task": "Add <kbd>flex: 2</kbd> to <kbd>.box2</kbd> to make it grow twice as wide.",
"previewHTML": "<div class='wrap'><div class='box box1'>1</div><div class='box box2'>2</div><div class='box box3'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; display: flex; align-items: center; justify-content: center; } .box1 { background: coral; flex: 1; } .box2 { background: mediumseagreen; } .box3 { background: gold; flex: 1; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; }",
"codePrefix": ".box2 {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "flex-wrap: wrap;",
"solution": "flex: 2;",
"previewContainer": "preview-area",
"concept": {
"explanation": "The flex property controls how flex items grow to fill available space. It's shorthand for flex-grow, flex-shrink, and flex-basis. When you set flex: 2, the item gets 2 \"shares\" of leftover space, while flex: 1 items get 1 share each. This creates proportional sizing based on the numbers you provide.",
"diagram": "flex: 2 vs flex: 1\n\n┌────────────────────────────┐\n│ Available Space │\n├────────┬──────────┬────────┤\n│ Box 1 │ Box 2 │ Box 3 │\n│ flex:1 │ flex:2 │ flex:1 │\n│ 25% │ 50% │ 25% │\n│ (1/4) │ (2/4) │ (1/4) │\n└────────┴──────────┴────────┘\n 1 share 2 shares 1 share",
"containerVsItem": "flex is an ITEM property, unlike the container properties we've seen. It's applied to individual children to control how they grow, not to the parent."
},
"validations": [
{
"type": "property_value",
"value": { "property": "flex-wrap", "expected": "wrap" },
"message": "Use the property that allows flex items to wrap onto new lines"
"value": {
"property": "flex",
"expected": "2"
},
"message": "Set <kbd>flex: 2</kbd>"
}
]
},
{
"id": "flexbox-6",
"title": "Flex Grow",
"description": "The <kbd>flex</kbd> property on items controls how they grow and shrink. <kbd>flex: 1</kbd> makes an item grow to fill available space. Multiple items with <kbd>flex: 1</kbd> share space equally.",
"task": "The search input is too narrow. Make it stretch to fill all the remaining space in the toolbar.",
"previewHTML": "<div class=\"toolbar\"><input class=\"search\" type=\"text\" placeholder=\"Search...\"><button class=\"btn\">Search</button><button class=\"btn\">Filters</button></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .toolbar { display: flex; gap: 8px; padding: 1rem; background: #f5f5f5; border-radius: 8px; } .search { padding: 8px 1rem; border: 1px solid #ddd; border-radius: 4px; font-size: 1rem; } .btn { padding: 8px 1rem; background: steelblue; color: white; border: none; border-radius: 4px; cursor: pointer; }",
"sandboxCSS": "",
"codePrefix": ".search {\n ",
"title": "Align Self",
"description": "Use <kbd>align-self</kbd> to override alignment for a single flex item.",
"task": "Add <kbd>align-self: flex-start</kbd> to <kbd>.middle</kbd> to move it to the top.",
"previewHTML": "<div class='wrap'><div class='box'>1</div><div class='box middle'>2</div><div class='box'>3</div></div>",
"previewBaseCSS": "body { font-family: system-ui, sans-serif; padding: 1rem; } .box { background: steelblue; color: white; padding: 1rem; margin: 8px; text-align: center; font-weight: bold; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; } .middle { background: mediumseagreen; }",
"sandboxCSS": ".wrap { border: 2px dashed #aaa; padding: 1rem; display: flex; height: 12rem; align-items: center; }",
"codePrefix": ".middle {\n ",
"initialCode": "",
"codeSuffix": "\n}",
"solution": "flex: 1;",
"solution": "align-self: flex-start;",
"previewContainer": "preview-area",
"concept": {
"explanation": "align-self allows a single flex item to override the container's align-items setting. While the container sets the default cross-axis alignment for all children, individual items can break free and align themselves differently. This is useful when one item needs special positioning without affecting the others.",
"diagram": "align-self: flex-start\n\n┌──────────────────────┐ ↑\n│ ┌─┐ │ │\n│ │2│ ← flex-start │ │\n│ └─┘ │ │ Cross\n│ ┌──┐ ┌──┐ │ │ Axis\n│ ──────│1 │────│3 │──│ ← center (default)\n│ └──┘ └──┘ │ │\n│ │ │\n│ │ ↓\n└──────────────────────┘",
"containerVsItem": "align-self is an ITEM property that overrides the container's align-items. This is the first property that gives individual children control over their own positioning."
},
"validations": [
{
"type": "regex",
"value": "(flex\\s*:\\s*1|flex-grow\\s*:\\s*1)",
"message": "Use the property that makes a flex item grow to fill available space"
"type": "property_value",
"value": {
"property": "align-self",
"expected": "flex-start"
},
"message": "Set <kbd>align-self: flex-start</kbd>"
}
]
}

Some files were not shown because too many files have changed in this diff Show More