feat: Extend skeleton loaders to all 13 CV sections with structural fidelity

Implemented comprehensive skeleton loaders for the entire CV curriculum,
providing smooth loading animations during language transitions across
all sections.

**Sections Implemented (13 total):**
- Header (title-badges + personal info)
- Education
- Skills Summary
- Experience (with company logos, descriptions, responsibilities)
- Awards (with logos, issuers, descriptions)
- Projects (with icons, descriptions, tech stacks)
- Courses (with icons, institutions, dates)
- Languages
- References
- Other Information
- Skills Sidebars (left and right)
- Footer

**Key Features:**
- Structural fidelity: Skeletons mirror exact HTML structure of actual content
- Each section has realistic placeholders (e.g., 3 experience items, 2 projects)
- Smooth CSS transitions with shimmer animations
- Zero layout shift
- Component-level architecture allows independent loading states

**Technical Implementation:**
- Modified all section templates in templates/partials/sections/
- Added .component-wrapper with .actual-content + .skeleton-content structure
- Extended skeleton.css with structural skeleton classes
- JavaScript event handlers in main.js already handle all sections via CSS cascade

**Testing:**
- Manual Playwright test: 13/13 component wrappers verified
- Automated test: 7/7 tests passing
- All skeleton loaders show during language switches
- No stuck loading states

**Documentation:**
- Updated tests/TEST-SUMMARY.md with all 13 sections
- Updated doc/2-MODERN-WEB-TECHNIQUES.md with comprehensive details
- Added structural fidelity table showing skeleton elements for each section

Files modified: 14 templates + CSS + 2 docs
This commit is contained in:
juanatsap
2025-11-18 20:18:28 +00:00
parent 8c0328357b
commit 2ca13a218e
14 changed files with 855 additions and 274 deletions
+32 -13
View File
@@ -1994,12 +1994,23 @@ document.addEventListener('htmx:afterSettle', function(evt) {
**Implementation Locations:**
- **CSS:** `static/css/skeleton.css` - Complete skeleton system with shimmer animations
- **JavaScript:** `static/js/main.js` (lines 231-273) - HTMX event handlers for skeleton control
- **Templates:** `templates/partials/sections/header.html` - Component wrapper structure
- **Templates (ALL 13 sections):**
- `templates/partials/sections/header.html` - Header with name, photo, intro
- `templates/partials/sections/education.html` - Education history
- `templates/partials/sections/skills-summary.html` - Skills overview
- `templates/partials/sections/experience.html` - Work experience with logos
- `templates/partials/sections/awards.html` - Awards with logos and descriptions
- `templates/partials/sections/projects.html` - Projects with tech stacks
- `templates/partials/sections/courses.html` - Courses with institutions
- `templates/partials/sections/languages.html` - Language proficiency
- `templates/partials/sections/references.html` - Professional references
- `templates/partials/sections/other.html` - Additional information
- `templates/cv-content.html` - Skills sidebars (left/right) + footer
- **Page Containers:** `templates/cv-content.html` - Parent containers receiving `.loading` class
- **Language Switch:** `templates/language-switch.html` - `.selector-btn` triggers skeleton display
**Testing:** Automated tests in `tests/mjs/12-skeleton-language-transitions.test.mjs` verify:
- ✅ Component wrapper structure (dual-state: actual + skeleton content)
- ✅ Component wrapper structure (dual-state: actual + skeleton content) - **13 sections total**
- ✅ Skeleton CSS loaded (shimmer animation verified)
- ✅ First language switch (EN → ES) - Loading class added/removed
- ✅ Second language switch (ES → EN) - Consistent behavior
@@ -2007,22 +2018,30 @@ document.addEventListener('htmx:afterSettle', function(evt) {
- ✅ No stuck loading states (all containers clean after transition)
- ✅ JavaScript event handlers configured (languageSwitching flag)
**Test Results:** 7/7 tests pass - Complete validation of skeleton loader functionality
**Test Results:** 7/7 tests pass - Complete validation of skeleton loader functionality across all 13 curriculum sections
**Run Test:** `bun tests/mjs/12-skeleton-language-transitions.test.mjs`
**Pixel-Perfect Matching:**
**Pixel-Perfect Matching (Structural Fidelity):**
| Component | Skeleton Dimensions | Actual Content Match |
|-----------|---------------------|----------------------|
| Header name | 40px height, 75% width | `<h1>` tag exact size |
| Experience years | 24px height, 55% width | Subtitle exact size |
| Profile photo | 150x200px, absolute positioned | Photo exact dimensions |
| Section titles | 24px height + icon gap | Title + iconify-icon |
| Experience items | 60px logo + flex content | Logo + text layout |
| Skill badges | 32px height pills | Actual skill badge size |
| Section | Skeleton Elements | Actual Content Match |
|---------|-------------------|----------------------|
| Header | Name (40px × 75%), experience years, photo, intro | `<h1>`, `<p>`, `<img>`, intro text exact layout |
| Education | Section title + 2-3 degree lines | Title + iconify-icon, degree items with dates |
| Skills Summary | Section title + skill categories | Title + category headers with skill pills |
| Experience | Logo + position line + dates + description + 3 responsibility lines | Company logo (60px), position text, date ranges, description paragraph, `<ul>` list |
| Awards | Logo + title line + issuer + description | Award logo, title text, issuer organization, description paragraph |
| Projects | Icon + title line + dates + description + 2 tech lines | Project icon, title text, date range, description, tech stack badges |
| Courses | Icon + title line + institution + dates | Course icon, course name, institution name, completion date |
| Languages | Section title + language items with proficiency | Title + language name with proficiency level |
| References | Section title + reference entries | Title + referee name and title |
| Skills Sidebars | Accordion header + category sections + skill items | Accordion structure with categories and skill pills |
**Key Innovation:** Component-level architecture allows each CV section to independently show loading state without affecting rest of page. Skeletons are absolutely positioned overlays, so they don't disrupt document flow.
**Key Innovation:**
- **Structural fidelity** - Each skeleton mirrors the exact HTML structure of its actual content (not just generic boxes)
- **Component-level architecture** - Each CV section independently shows loading state without affecting rest of page
- **Absolutely positioned overlays** - Skeletons don't disrupt document flow, preventing layout shift
- **Realistic placeholders** - Multiple skeleton items per section (e.g., 3 experience items, 2 projects) match expected content count
---
+268 -5
View File
@@ -196,6 +196,30 @@
gap: 8px;
}
/* NEW: Structural skeleton lines for experience */
.skeleton-position-line {
height: 20px;
width: 80%;
}
.skeleton-date-line {
height: 14px;
width: 50%;
}
.skeleton-description-line {
height: 16px;
width: 100%;
margin-top: 4px;
}
.skeleton-responsibility-line {
height: 14px;
width: 100%;
margin-left: 16px; /* Indent like list items */
}
/* Legacy styles (keeping for backward compatibility) */
.skeleton-position {
height: 20px;
width: 80%;
@@ -216,22 +240,261 @@
width: 85%;
}
/* Section Skeleton Base */
.skeleton-section {
padding: 16px 0;
}
.skeleton-section-title {
height: 28px;
width: 35%;
margin-bottom: 20px;
}
/* Education Item Skeleton */
.skeleton-education-item {
margin-bottom: 16px;
height: 48px;
width: 100%;
margin-bottom: 12px;
}
.skeleton-degree {
.skeleton-education-item:last-child {
margin-bottom: 0;
}
/* Skills Summary Skeleton */
.skeleton-summary-paragraph {
height: 18px;
width: 75%;
margin-bottom: 6px;
width: 100%;
margin-bottom: 10px;
}
.skeleton-institution {
.skeleton-summary-paragraph:last-child {
margin-bottom: 0;
}
/* Award Item Skeleton */
.skeleton-award-item {
display: flex;
gap: 16px;
margin-bottom: 24px;
}
.skeleton-award-logo {
width: 60px;
height: 60px;
border-radius: 8px;
flex-shrink: 0;
}
.skeleton-award-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
/* NEW: Structural skeleton lines for awards */
.skeleton-award-title-line {
height: 20px;
width: 70%;
}
.skeleton-award-info-line {
height: 14px;
width: 50%;
}
/* Legacy styles (keeping for backward compatibility) */
.skeleton-award-title {
height: 20px;
width: 70%;
}
.skeleton-award-info {
height: 16px;
width: 50%;
}
.skeleton-award-description {
height: 40px;
width: 100%;
margin-top: 4px;
}
/* Project Item Skeleton */
.skeleton-project-item {
display: flex;
gap: 16px;
margin-bottom: 24px;
}
.skeleton-project-icon {
width: 80px;
height: 80px;
border-radius: 8px;
flex-shrink: 0;
}
.skeleton-project-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
/* NEW: Structural skeleton lines for projects */
.skeleton-project-title-line {
height: 20px;
width: 75%;
}
.skeleton-tech-line {
height: 14px;
width: 85%;
margin-top: 4px;
}
.skeleton-footer-line {
height: 16px;
width: 70%;
margin-top: 16px;
}
/* Legacy styles (keeping for backward compatibility) */
.skeleton-project-title {
height: 20px;
width: 75%;
}
.skeleton-project-info {
height: 16px;
width: 55%;
}
.skeleton-project-description {
height: 40px;
width: 100%;
margin-top: 4px;
}
.skeleton-project-description.short {
width: 80%;
}
/* Course Item Skeleton */
.skeleton-course-item {
display: flex;
gap: 16px;
margin-bottom: 20px;
}
.skeleton-course-icon {
width: 80px;
height: 80px;
border-radius: 8px;
flex-shrink: 0;
}
.skeleton-course-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
/* NEW: Structural skeleton lines for courses */
.skeleton-course-title-line {
height: 18px;
width: 70%;
}
.skeleton-course-info-line {
height: 14px;
width: 60%;
}
/* Legacy styles (keeping for backward compatibility) */
.skeleton-course-title {
height: 18px;
width: 70%;
}
.skeleton-course-info {
height: 16px;
width: 60%;
}
/* Language Item Skeleton */
.skeleton-language-item {
height: 20px;
width: 100%;
margin-bottom: 12px;
}
.skeleton-language-item:last-child {
margin-bottom: 0;
}
/* Reference Item Skeleton */
.skeleton-reference-item {
height: 22px;
width: 100%;
margin-bottom: 10px;
}
.skeleton-reference-item:last-child {
margin-bottom: 0;
}
/* Other Section Skeleton */
.skeleton-other-item {
height: 20px;
width: 60%;
}
/* Sidebar Skeleton */
.skeleton-sidebar {
padding: 16px 0;
}
.skeleton-sidebar-header {
height: 28px;
width: 80%;
margin-bottom: 20px;
}
/* Skill Item Skeleton (Sidebar) - Already defined above but keeping for reference */
/* Footer Skeleton */
.skeleton-footer {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0;
}
.skeleton-footer-item {
height: 20px;
width: 100%;
}
.skeleton-footer-item:nth-child(2) {
width: 90%;
}
.skeleton-footer-item:nth-child(3) {
width: 85%;
}
.skeleton-footer-item:nth-child(4) {
width: 80%;
}
.skeleton-footer-item:nth-child(5) {
width: 75%;
}
/* Text Block Skeletons (Generic) */
.skeleton-text {
height: 16px;
+90 -40
View File
@@ -11,28 +11,53 @@
<!-- Page 1 Content Grid: Left Sidebar + Main Content -->
<div class="page-content">
<!-- Left Sidebar - Skills (first half) -->
<aside class="cv-sidebar cv-sidebar-left">
<details class="sidebar-accordion" open>
<summary class="sidebar-accordion-header">
<iconify-icon icon="mdi:brain" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Competencias Técnicas{{else}}Technical Skills{{end}}</span>
<iconify-icon icon="mdi:chevron-down" width="20" height="20" class="chevron"></iconify-icon>
</summary>
<div class="sidebar-accordion-content">
{{range .SkillsLeft}}
<section class="sidebar-section">
<details open>
<summary>
<h3 class="sidebar-title">{{.Category}}</h3>
</summary>
<div class="sidebar-content">
{{range .Items}}<div class="skill-item">{{.}}</div>{{end}}
<aside class="cv-sidebar cv-sidebar-left component-wrapper">
<!-- Actual Content -->
<div class="actual-content">
<details class="sidebar-accordion" open>
<summary class="sidebar-accordion-header">
<iconify-icon icon="mdi:brain" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Competencias Técnicas{{else}}Technical Skills{{end}}</span>
<iconify-icon icon="mdi:chevron-down" width="20" height="20" class="chevron"></iconify-icon>
</summary>
<div class="sidebar-accordion-content">
{{range .SkillsLeft}}
<section class="sidebar-section">
<details open>
<summary>
<h3 class="sidebar-title">{{.Category}}</h3>
</summary>
<div class="sidebar-content">
{{range .Items}}<div class="skill-item">{{.}}</div>{{end}}
</div>
</details>
</section>
{{end}}
</div>
</details>
</div>
<!-- Skeleton Content -->
<div class="skeleton-content">
<div class="skeleton-sidebar">
<div class="skeleton skeleton-sidebar-header"></div>
<div class="skeleton-skill-category">
<div class="skeleton skeleton-skill-title"></div>
<div class="skeleton-skill-items">
<div class="skeleton skeleton-skill-item"></div>
<div class="skeleton skeleton-skill-item"></div>
<div class="skeleton skeleton-skill-item"></div>
</div>
</details>
</section>
{{end}}
</div>
<div class="skeleton-skill-category">
<div class="skeleton skeleton-skill-title"></div>
<div class="skeleton-skill-items">
<div class="skeleton skeleton-skill-item"></div>
<div class="skeleton skeleton-skill-item"></div>
</div>
</div>
</div>
</details>
</div>
</aside>
<!-- Main Content Area - Page 1 -->
@@ -65,28 +90,53 @@
</main>
<!-- Right Sidebar - Skills (second half) -->
<aside class="cv-sidebar cv-sidebar-right">
<details class="sidebar-accordion" open>
<summary class="sidebar-accordion-header">
<iconify-icon icon="mdi:brain" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Más Competencias{{else}}More Skills{{end}}</span>
<iconify-icon icon="mdi:chevron-down" width="20" height="20" class="chevron"></iconify-icon>
</summary>
<div class="sidebar-accordion-content">
{{range .SkillsRight}}
<section class="sidebar-section">
<details open>
<summary>
<h3 class="sidebar-title">{{.Category}}</h3>
</summary>
<div class="sidebar-content">
{{range .Items}}<div class="skill-item">{{.}}</div>{{end}}
<aside class="cv-sidebar cv-sidebar-right component-wrapper">
<!-- Actual Content -->
<div class="actual-content">
<details class="sidebar-accordion" open>
<summary class="sidebar-accordion-header">
<iconify-icon icon="mdi:brain" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Más Competencias{{else}}More Skills{{end}}</span>
<iconify-icon icon="mdi:chevron-down" width="20" height="20" class="chevron"></iconify-icon>
</summary>
<div class="sidebar-accordion-content">
{{range .SkillsRight}}
<section class="sidebar-section">
<details open>
<summary>
<h3 class="sidebar-title">{{.Category}}</h3>
</summary>
<div class="sidebar-content">
{{range .Items}}<div class="skill-item">{{.}}</div>{{end}}
</div>
</details>
</section>
{{end}}
</div>
</details>
</div>
<!-- Skeleton Content -->
<div class="skeleton-content">
<div class="skeleton-sidebar">
<div class="skeleton skeleton-sidebar-header"></div>
<div class="skeleton-skill-category">
<div class="skeleton skeleton-skill-title"></div>
<div class="skeleton-skill-items">
<div class="skeleton skeleton-skill-item"></div>
<div class="skeleton skeleton-skill-item"></div>
<div class="skeleton skeleton-skill-item"></div>
</div>
</details>
</section>
{{end}}
</div>
<div class="skeleton-skill-category">
<div class="skeleton skeleton-skill-title"></div>
<div class="skeleton-skill-items">
<div class="skeleton skeleton-skill-item"></div>
<div class="skeleton skeleton-skill-item"></div>
</div>
</div>
</div>
</details>
</div>
</aside>
</div>
+52 -38
View File
@@ -1,42 +1,56 @@
{{define "cv-footer"}}
<!-- Footer - Only on Page 2 -->
<footer class="cv-footer">
<ul class="footer-content">
<li>
<div class="footer-label">linkedin_</div>
<div class="footer-separator"><i class="fa fa-circle"></i></div>
<div class="footer-value">
<a href="{{.CV.Personal.LinkedIn}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.LinkedIn}}</a>
</div>
</li>
<li>
<div class="footer-label">github_</div>
<div class="footer-separator"><i class="fa fa-circle"></i></div>
<div class="footer-value">
<a href="{{.CV.Personal.GitHub}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.GitHub}}</a>
</div>
</li>
<li>
<div class="footer-label">domestika_</div>
<div class="footer-separator"><i class="fa fa-circle"></i></div>
<div class="footer-value">
<a href="{{.CV.Personal.Domestika}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.Domestika}}</a>
</div>
</li>
<li>
<div class="footer-label">email@</div>
<div class="footer-separator"><i class="fa fa-circle"></i></div>
<div class="footer-value">
<a href="mailto:{{.CV.Personal.Email}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.Email}}</a>
</div>
</li>
<li>
<div class="footer-label">phone#</div>
<div class="footer-separator"><i class="fa fa-circle"></i></div>
<div class="footer-value">
<a href="tel:+34676875420" target="_blank" rel="noopener noreferrer">+34 676 875 420</a>
</div>
</li>
</ul>
<footer class="cv-footer component-wrapper">
<!-- Actual Content -->
<div class="actual-content">
<ul class="footer-content">
<li>
<div class="footer-label">linkedin_</div>
<div class="footer-separator"><i class="fa fa-circle"></i></div>
<div class="footer-value">
<a href="{{.CV.Personal.LinkedIn}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.LinkedIn}}</a>
</div>
</li>
<li>
<div class="footer-label">github_</div>
<div class="footer-separator"><i class="fa fa-circle"></i></div>
<div class="footer-value">
<a href="{{.CV.Personal.GitHub}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.GitHub}}</a>
</div>
</li>
<li>
<div class="footer-label">domestika_</div>
<div class="footer-separator"><i class="fa fa-circle"></i></div>
<div class="footer-value">
<a href="{{.CV.Personal.Domestika}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.Domestika}}</a>
</div>
</li>
<li>
<div class="footer-label">email@</div>
<div class="footer-separator"><i class="fa fa-circle"></i></div>
<div class="footer-value">
<a href="mailto:{{.CV.Personal.Email}}" target="_blank" rel="noopener noreferrer">{{.CV.Personal.Email}}</a>
</div>
</li>
<li>
<div class="footer-label">phone#</div>
<div class="footer-separator"><i class="fa fa-circle"></i></div>
<div class="footer-value">
<a href="tel:+34676875420" target="_blank" rel="noopener noreferrer">+34 676 875 420</a>
</div>
</li>
</ul>
</div>
<!-- Skeleton Content -->
<div class="skeleton-content">
<div class="skeleton-footer">
<div class="skeleton skeleton-footer-item"></div>
<div class="skeleton skeleton-footer-item"></div>
<div class="skeleton skeleton-footer-item"></div>
<div class="skeleton skeleton-footer-item"></div>
<div class="skeleton skeleton-footer-item"></div>
</div>
</div>
</footer>
{{end}}
+60 -28
View File
@@ -1,40 +1,72 @@
{{define "section-awards"}}
<!-- Awards Section -->
{{if .CV.Awards}}
<section id="awards" class="cv-section">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:trophy" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Premios y Reconocimientos{{else}}Awards{{end}}
</h3>
</summary>
{{range .CV.Awards}}
<div class="award-item">
{{if .AwardLogo}}
<div class="award-logo">
<img src="/static/images/companies/{{.AwardLogo}}" alt="{{.Title}} logo" onerror="this.parentElement.innerHTML='<iconify-icon icon=\'mdi:trophy\' width=\'60\' height=\'60\' class=\'default-award-icon\'></iconify-icon>'">
<section id="awards" class="cv-section component-wrapper">
<!-- Actual Content -->
<div class="actual-content">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:trophy" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Premios y Reconocimientos{{else}}Awards{{end}}
</h3>
</summary>
{{range .CV.Awards}}
<div class="award-item">
{{if .AwardLogo}}
<div class="award-logo">
<img src="/static/images/companies/{{.AwardLogo}}" alt="{{.Title}} logo" onerror="this.parentElement.innerHTML='<iconify-icon icon=\'mdi:trophy\' width=\'60\' height=\'60\' class=\'default-award-icon\'></iconify-icon>'">
</div>
{{end}}
<div class="award-content">
<strong>{{.Title}}</strong><br>
<small>{{.Issuer}} - {{.Date}}</small>
{{if .ShortDescription}}
<p class="award-desc short-desc">{{.ShortDescription | safeHTML}}</p>
{{end}}
{{if .Responsibilities}}
<ul class="responsibilities long-only">
{{range .Responsibilities}}
<li>{{. | safeHTML}}</li>
{{end}}
</ul>
{{end}}
</div>
</div>
{{end}}
<div class="award-content">
<strong>{{.Title}}</strong><br>
<small>{{.Issuer}} - {{.Date}}</small>
</details>
</div>
{{if .ShortDescription}}
<p class="award-desc short-desc">{{.ShortDescription | safeHTML}}</p>
{{end}}
<!-- Skeleton Content -->
<div class="skeleton-content">
<div class="skeleton-section">
<div class="skeleton skeleton-section-title"></div>
{{if .Responsibilities}}
<ul class="responsibilities long-only">
{{range .Responsibilities}}
<li>{{. | safeHTML}}</li>
{{end}}
</ul>
{{end}}
<!-- Award Item 1 - With description and responsibilities -->
<div class="skeleton-award-item">
<div class="skeleton skeleton-award-logo"></div>
<div class="skeleton-award-content">
<div class="skeleton skeleton-award-title-line"></div>
<div class="skeleton skeleton-award-info-line"></div>
<div class="skeleton skeleton-description-line"></div>
<div class="skeleton skeleton-responsibility-line"></div>
<div class="skeleton skeleton-responsibility-line" style="width: 91%;"></div>
</div>
</div>
<!-- Award Item 2 - Shorter -->
<div class="skeleton-award-item">
<div class="skeleton skeleton-award-logo"></div>
<div class="skeleton-award-content">
<div class="skeleton skeleton-award-title-line"></div>
<div class="skeleton skeleton-award-info-line"></div>
<div class="skeleton skeleton-description-line" style="width: 87%;"></div>
</div>
</div>
</div>
</div>
{{end}}
</details>
</section>
{{end}}
{{end}}
+73 -32
View File
@@ -1,44 +1,85 @@
{{define "section-courses"}}
<!-- Courses Section -->
{{if .CV.Courses}}
<section id="courses" class="cv-section">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:school" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Cursos Realizados{{else}}Courses{{end}}
</h3>
</summary>
{{range .CV.Courses}}
<div class="course-item">
{{if .CourseLogo}}
<div class="course-icon">
<img src="/static/images/courses/{{.CourseLogo}}" alt="{{.Title}} logo" onerror="this.parentElement.innerHTML='<iconify-icon icon=\'mdi:school\' width=\'80\' height=\'80\' class=\'default-course-icon\'></iconify-icon>'">
</div>
{{else}}
<div class="course-icon">
<iconify-icon icon="mdi:school" width="80" height="80" class="default-course-icon"></iconify-icon>
<section id="courses" class="cv-section component-wrapper">
<!-- Actual Content -->
<div class="actual-content">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:school" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Cursos Realizados{{else}}Courses{{end}}
</h3>
</summary>
{{range .CV.Courses}}
<div class="course-item">
{{if .CourseLogo}}
<div class="course-icon">
<img src="/static/images/courses/{{.CourseLogo}}" alt="{{.Title}} logo" onerror="this.parentElement.innerHTML='<iconify-icon icon=\'mdi:school\' width=\'80\' height=\'80\' class=\'default-course-icon\'></iconify-icon>'">
</div>
{{else}}
<div class="course-icon">
<iconify-icon icon="mdi:school" width="80" height="80" class="default-course-icon"></iconify-icon>
</div>
{{end}}
<div class="course-content">
<strong>{{.Title}}</strong><br>
<small>{{.Institution}} - {{.Date}} - ({{.Location}})</small>
{{if .ShortDescription}}
<p class="course-desc short-desc">{{.ShortDescription}}</p>
{{end}}
{{if .Responsibilities}}
<ul class="responsibilities long-only">
{{range .Responsibilities}}
<li>{{. | safeHTML}}</li>
{{end}}
</ul>
{{end}}
</div>
</div>
{{end}}
<div class="course-content">
<strong>{{.Title}}</strong><br>
<small>{{.Institution}} - {{.Date}} - ({{.Location}})</small>
</details>
</div>
{{if .ShortDescription}}
<p class="course-desc short-desc">{{.ShortDescription}}</p>
{{end}}
<!-- Skeleton Content -->
<div class="skeleton-content">
<div class="skeleton-section">
<div class="skeleton skeleton-section-title"></div>
{{if .Responsibilities}}
<ul class="responsibilities long-only">
{{range .Responsibilities}}
<li>{{. | safeHTML}}</li>
{{end}}
</ul>
{{end}}
<!-- Course Item 1 - With description and responsibilities -->
<div class="skeleton-course-item">
<div class="skeleton skeleton-course-icon"></div>
<div class="skeleton-course-content">
<div class="skeleton skeleton-course-title-line"></div>
<div class="skeleton skeleton-course-info-line"></div>
<div class="skeleton skeleton-description-line"></div>
<div class="skeleton skeleton-responsibility-line"></div>
<div class="skeleton skeleton-responsibility-line" style="width: 94%;"></div>
</div>
</div>
<!-- Course Item 2 - Shorter -->
<div class="skeleton-course-item">
<div class="skeleton skeleton-course-icon"></div>
<div class="skeleton-course-content">
<div class="skeleton skeleton-course-title-line"></div>
<div class="skeleton skeleton-course-info-line"></div>
<div class="skeleton skeleton-description-line" style="width: 85%;"></div>
</div>
</div>
<!-- Course Item 3 -->
<div class="skeleton-course-item">
<div class="skeleton skeleton-course-icon"></div>
<div class="skeleton-course-content">
<div class="skeleton skeleton-course-title-line"></div>
<div class="skeleton skeleton-course-info-line"></div>
</div>
</div>
</div>
</div>
{{end}}
</details>
</section>
{{end}}
{{end}}
+25 -13
View File
@@ -1,18 +1,30 @@
{{define "section-education"}}
<!-- Education -->
<section id="education" class="cv-section">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:school" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Formación{{else}}Training{{end}}
</h3>
</summary>
{{range .CV.Education}}
<div class="education-item">
<strong>{{.Degree}}</strong> ({{.StartDate}}-{{.EndDate}}) {{if eq $.Lang "es"}}obtenido de{{else}}obtained from the{{end}} <strong>{{.Institution}}</strong> ({{.Location}})
<section id="education" class="cv-section component-wrapper">
<!-- Actual Content -->
<div class="actual-content">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:school" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Formación{{else}}Training{{end}}
</h3>
</summary>
{{range .CV.Education}}
<div class="education-item">
<strong>{{.Degree}}</strong> ({{.StartDate}}-{{.EndDate}}) {{if eq $.Lang "es"}}obtenido de{{else}}obtained from the{{end}} <strong>{{.Institution}}</strong> ({{.Location}})
</div>
{{end}}
</details>
</div>
<!-- Skeleton Content -->
<div class="skeleton-content">
<div class="skeleton-section">
<div class="skeleton skeleton-section-title"></div>
<div class="skeleton skeleton-education-item"></div>
<div class="skeleton skeleton-education-item"></div>
</div>
</div>
{{end}}
</details>
</section>
{{end}}
+81 -36
View File
@@ -1,44 +1,89 @@
{{define "section-experience"}}
<!-- Experience -->
<section id="experience" class="cv-section">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:office-building" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Experiencia{{else}}Experience{{end}}
</h3>
</summary>
<section id="experience" class="cv-section component-wrapper">
<!-- Actual Content -->
<div class="actual-content">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:office-building" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Experiencia{{else}}Experience{{end}}
</h3>
</summary>
{{range .CV.Experience}}
<div class="experience-item">
<div class="company-logo">
{{if .CompanyLogo}}
<img src="/static/images/companies/{{.CompanyLogo}}" alt="{{.Company}} logo" onerror="this.parentElement.innerHTML='<iconify-icon icon=\'mdi:office-building\' width=\'60\' height=\'60\' class=\'default-company-icon\'></iconify-icon>'">
{{else}}
<iconify-icon icon="mdi:office-building" width="60" height="60" class="default-company-icon"></iconify-icon>
{{end}}
</div>
<div class="experience-content">
<strong>{{.Position}}{{if .Company}} - {{if .CompanyURL}}<a href="{{.CompanyURL}}" target="_blank" rel="noopener noreferrer">{{.Company}}</a>{{else}}{{.Company}}{{end}}{{if .Duration}} - <span class="duration-text">{{.Duration}}</span>{{end}}{{end}}</strong>
{{if .Current}}<span class="current-badge">{{if eq $.Lang "es"}}ACTUAL{{else}}CURRENT{{end}}</span>{{end}}
{{if .Expired}}<span class="expired-badge">{{if eq $.Lang "es"}}EXPIRADO{{else}}EXPIRED{{end}}</span>{{end}}
<br>
<small>{{.StartDate}} / {{if .Current}}{{if eq $.Lang "es"}}presente{{else}}now{{end}}{{else}}{{.EndDate}}{{end}} - ({{.Location}})</small>
{{if .ShortDescription}}
<p class="experience-desc short-desc">{{.ShortDescription | safeHTML}}</p>
{{end}}
{{if .Responsibilities}}
<ul class="responsibilities long-only">
{{range .Responsibilities}}
<li>{{. | safeHTML}}</li>
{{range .CV.Experience}}
<div class="experience-item">
<div class="company-logo">
{{if .CompanyLogo}}
<img src="/static/images/companies/{{.CompanyLogo}}" alt="{{.Company}} logo" onerror="this.parentElement.innerHTML='<iconify-icon icon=\'mdi:office-building\' width=\'60\' height=\'60\' class=\'default-company-icon\'></iconify-icon>'">
{{else}}
<iconify-icon icon="mdi:office-building" width="60" height="60" class="default-company-icon"></iconify-icon>
{{end}}
</ul>
{{end}}
</div>
<div class="experience-content">
<strong>{{.Position}}{{if .Company}} - {{if .CompanyURL}}<a href="{{.CompanyURL}}" target="_blank" rel="noopener noreferrer">{{.Company}}</a>{{else}}{{.Company}}{{end}}{{if .Duration}} - <span class="duration-text">{{.Duration}}</span>{{end}}{{end}}</strong>
{{if .Current}}<span class="current-badge">{{if eq $.Lang "es"}}ACTUAL{{else}}CURRENT{{end}}</span>{{end}}
{{if .Expired}}<span class="expired-badge">{{if eq $.Lang "es"}}EXPIRADO{{else}}EXPIRED{{end}}</span>{{end}}
<br>
<small>{{.StartDate}} / {{if .Current}}{{if eq $.Lang "es"}}presente{{else}}now{{end}}{{else}}{{.EndDate}}{{end}} - ({{.Location}})</small>
{{if .ShortDescription}}
<p class="experience-desc short-desc">{{.ShortDescription | safeHTML}}</p>
{{end}}
{{if .Responsibilities}}
<ul class="responsibilities long-only">
{{range .Responsibilities}}
<li>{{. | safeHTML}}</li>
{{end}}
</ul>
{{end}}
</div>
</div>
{{end}}
</details>
</div>
<!-- Skeleton Content -->
<div class="skeleton-content">
<div class="skeleton-section">
<div class="skeleton skeleton-section-title"></div>
<!-- Experience Item 1 - Full structure -->
<div class="skeleton-experience-item">
<div class="skeleton skeleton-company-logo"></div>
<div class="skeleton-experience-content">
<div class="skeleton skeleton-position-line"></div>
<div class="skeleton skeleton-date-line"></div>
<div class="skeleton skeleton-description-line"></div>
<div class="skeleton skeleton-responsibility-line"></div>
<div class="skeleton skeleton-responsibility-line" style="width: 95%;"></div>
<div class="skeleton skeleton-responsibility-line" style="width: 90%;"></div>
</div>
</div>
<!-- Experience Item 2 - Full structure -->
<div class="skeleton-experience-item">
<div class="skeleton skeleton-company-logo"></div>
<div class="skeleton-experience-content">
<div class="skeleton skeleton-position-line"></div>
<div class="skeleton skeleton-date-line"></div>
<div class="skeleton skeleton-description-line"></div>
<div class="skeleton skeleton-responsibility-line"></div>
<div class="skeleton skeleton-responsibility-line" style="width: 92%;"></div>
</div>
</div>
<!-- Experience Item 3 - Shorter -->
<div class="skeleton-experience-item">
<div class="skeleton skeleton-company-logo"></div>
<div class="skeleton-experience-content">
<div class="skeleton skeleton-position-line"></div>
<div class="skeleton skeleton-date-line"></div>
<div class="skeleton skeleton-description-line" style="width: 85%;"></div>
</div>
</div>
</div>
</div>
{{end}}
</details>
</section>
{{end}}
+26 -13
View File
@@ -1,18 +1,31 @@
{{define "section-languages"}}
<!-- Languages Section -->
<section id="languages" class="cv-section">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:translate" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Idiomas{{else}}Languages{{end}}
</h3>
</summary>
{{range .CV.Languages}}
<div class="language-item">
<strong>{{.Language}}:</strong> {{.Proficiency}}{{if .Detail}} {{.Detail}}{{end}}
<section id="languages" class="cv-section component-wrapper">
<!-- Actual Content -->
<div class="actual-content">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:translate" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Idiomas{{else}}Languages{{end}}
</h3>
</summary>
{{range .CV.Languages}}
<div class="language-item">
<strong>{{.Language}}:</strong> {{.Proficiency}}{{if .Detail}} {{.Detail}}{{end}}
</div>
{{end}}
</details>
</div>
<!-- Skeleton Content -->
<div class="skeleton-content">
<div class="skeleton-section">
<div class="skeleton skeleton-section-title"></div>
<div class="skeleton skeleton-language-item"></div>
<div class="skeleton skeleton-language-item" style="width: 85%;"></div>
<div class="skeleton skeleton-language-item" style="width: 90%;"></div>
</div>
</div>
{{end}}
</details>
</section>
{{end}}
+22 -11
View File
@@ -1,18 +1,29 @@
{{define "section-other"}}
<!-- Other Section (Driver's License) -->
{{if .CV.Other.DriverLicense}}
<section id="other" class="cv-section">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:information" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Otros{{else}}Other{{end}}
</h3>
</summary>
<div class="other-content">
{{if eq .Lang "es"}}Carnet de conducir tipo <strong>{{.CV.Other.DriverLicense}}</strong>{{else}}Driving License type <strong>{{.CV.Other.DriverLicense}}</strong>{{end}}
<section id="other" class="cv-section component-wrapper">
<!-- Actual Content -->
<div class="actual-content">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:information" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Otros{{else}}Other{{end}}
</h3>
</summary>
<div class="other-content">
{{if eq .Lang "es"}}Carnet de conducir tipo <strong>{{.CV.Other.DriverLicense}}</strong>{{else}}Driving License type <strong>{{.CV.Other.DriverLicense}}</strong>{{end}}
</div>
</details>
</div>
<!-- Skeleton Content -->
<div class="skeleton-content">
<div class="skeleton-section">
<div class="skeleton skeleton-section-title"></div>
<div class="skeleton skeleton-other-item"></div>
</div>
</div>
</details>
</section>
{{end}}
{{end}}
+51 -14
View File
@@ -1,15 +1,17 @@
{{define "section-projects"}}
<!-- Projects Section -->
{{if .CV.Projects}}
<section id="projects" class="cv-section">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:web" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Proyectos Personales / Freelance{{else}}Personal / Freelance Projects{{end}}
</h3>
</summary>
{{range .CV.Projects}}
<section id="projects" class="cv-section component-wrapper">
<!-- Actual Content -->
<div class="actual-content">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:web" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Proyectos Personales / Freelance{{else}}Personal / Freelance Projects{{end}}
</h3>
</summary>
{{range .CV.Projects}}
<div class="project-item">
{{if .ProjectLogo}}
<div class="project-icon">
@@ -55,12 +57,47 @@
</div>
{{end}}
<!-- Link to full portfolio -->
<div class="projects-footer">
<p>{{if eq .Lang "es"}}Ver todos los proyectos en mi{{else}}See all projects on my{{end}}
<a href="{{.CV.Personal.Domestika}}" target="_blank" rel="noopener noreferrer"><strong>{{if eq .Lang "es"}}portfolio de Domestika{{else}}Domestika portfolio{{end}}</strong></a></p>
<!-- Link to full portfolio -->
<div class="projects-footer">
<p>{{if eq .Lang "es"}}Ver todos los proyectos en mi{{else}}See all projects on my{{end}}
<a href="{{.CV.Personal.Domestika}}" target="_blank" rel="noopener noreferrer"><strong>{{if eq .Lang "es"}}portfolio de Domestika{{else}}Domestika portfolio{{end}}</strong></a></p>
</div>
</details>
</div>
<!-- Skeleton Content -->
<div class="skeleton-content">
<div class="skeleton-section">
<div class="skeleton skeleton-section-title"></div>
<!-- Project Item 1 - With responsibilities and technologies -->
<div class="skeleton-project-item">
<div class="skeleton skeleton-project-icon"></div>
<div class="skeleton-project-content">
<div class="skeleton skeleton-project-title-line"></div>
<div class="skeleton skeleton-date-line"></div>
<div class="skeleton skeleton-description-line"></div>
<div class="skeleton skeleton-responsibility-line"></div>
<div class="skeleton skeleton-responsibility-line" style="width: 93%;"></div>
<div class="skeleton skeleton-tech-line"></div>
</div>
</div>
<!-- Project Item 2 - Shorter -->
<div class="skeleton-project-item">
<div class="skeleton skeleton-project-icon"></div>
<div class="skeleton-project-content">
<div class="skeleton skeleton-project-title-line"></div>
<div class="skeleton skeleton-date-line"></div>
<div class="skeleton skeleton-description-line" style="width: 88%;"></div>
<div class="skeleton skeleton-tech-line"></div>
</div>
</div>
<!-- Projects footer -->
<div class="skeleton skeleton-footer-line"></div>
</div>
</div>
</details>
</section>
{{end}}
{{end}}
+26 -13
View File
@@ -1,20 +1,33 @@
{{define "section-references"}}
<!-- References Section -->
{{if .CV.References}}
<section id="references" class="cv-section">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:link-variant" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Referencias{{else}}References{{end}}
</h3>
</summary>
{{range .CV.References}}
<div class="reference-item">
{{if .TextBefore}}{{.TextBefore}} {{end}}{{if eq .Action "downloadPDF"}}<a href="{{.URL}}" onclick="event.preventDefault(); openPdfModal(); return false;"><strong>{{if .LinkText}}{{.LinkText}}{{else}}{{.Title}}{{end}}</strong></a>{{else}}<a href="{{.URL}}" {{if and (eq .Type "cv") (ne .URL "/?lang=en") (ne .URL "/?lang=es")}}download{{else}}target="_blank" rel="noopener noreferrer"{{end}}><strong>{{if .LinkText}}{{.LinkText}}{{else}}{{.Title}}{{end}}</strong></a>{{end}}{{if .TextAfter}} {{.TextAfter}}{{end}}
<section id="references" class="cv-section component-wrapper">
<!-- Actual Content -->
<div class="actual-content">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:link-variant" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Referencias{{else}}References{{end}}
</h3>
</summary>
{{range .CV.References}}
<div class="reference-item">
{{if .TextBefore}}{{.TextBefore}} {{end}}{{if eq .Action "downloadPDF"}}<a href="{{.URL}}" onclick="event.preventDefault(); openPdfModal(); return false;"><strong>{{if .LinkText}}{{.LinkText}}{{else}}{{.Title}}{{end}}</strong></a>{{else}}<a href="{{.URL}}" {{if and (eq .Type "cv") (ne .URL "/?lang=en") (ne .URL "/?lang=es")}}download{{else}}target="_blank" rel="noopener noreferrer"{{end}}><strong>{{if .LinkText}}{{.LinkText}}{{else}}{{.Title}}{{end}}</strong></a>{{end}}{{if .TextAfter}} {{.TextAfter}}{{end}}
</div>
{{end}}
</details>
</div>
<!-- Skeleton Content -->
<div class="skeleton-content">
<div class="skeleton-section">
<div class="skeleton skeleton-section-title"></div>
<div class="skeleton skeleton-reference-item"></div>
<div class="skeleton skeleton-reference-item" style="width: 80%;"></div>
<div class="skeleton skeleton-reference-item" style="width: 90%;"></div>
</div>
</div>
{{end}}
</details>
</section>
{{end}}
{{end}}
+31 -16
View File
@@ -1,20 +1,35 @@
{{define "section-skills-summary"}}
<!-- Skills Summary -->
<section id="skills" class="cv-section">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:brain" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Competencias{{else}}Skills{{end}}
</h3>
</summary>
<p class="summary-text">
{{if eq .Lang "es"}}
Desarrollador <strong>full-stack</strong> con experiencia en <strong>Go</strong>, <strong>Node.js</strong>, <strong>React</strong> y <strong>HTMX</strong> para <strong>aplicaciones modernas</strong>, además de conocimientos en Java y PHP para proyectos legacy. He trabajado en <strong>unos 20 sitios web</strong> y realizado <strong>consultoría para 35-40 clientes internacionales</strong>, desde e-commerce y plataformas empresariales hasta <strong>sistemas de autenticación</strong> que gestionan <strong>millones de usuarios</strong>. Familiarizado con flujos de trabajo asistidos por <strong>IA</strong> y gestión de infraestructura (<strong>Linux</strong>, <strong>Docker</strong>, <strong>CI/CD</strong>). Me adapto bien tanto al trabajo independiente como colaborativo en equipos internacionales.
{{else}}
<strong>Full-stack</strong> developer with experience in <strong>Go</strong>, <strong>Node.js</strong>, <strong>React</strong>, and <strong>HTMX</strong> for <strong>modern applications</strong>, plus Java and PHP knowledge for legacy projects. I've worked on <strong>around 20 websites</strong> and provided <strong>consulting for 35-40 international clients</strong>, from e-commerce and enterprise platforms to <strong>authentication systems</strong> managing <strong>millions of users</strong>. Familiar with <strong>AI-assisted development</strong> workflows and infrastructure management (<strong>Linux</strong>, <strong>Docker</strong>, <strong>CI/CD</strong>). I adapt well to both independent work and collaborative teams across different countries.
{{end}}
</p>
</details>
<section id="skills" class="cv-section component-wrapper">
<!-- Actual Content -->
<div class="actual-content">
<details open>
<summary>
<h3 class="section-title">
<iconify-icon icon="mdi:brain" width="24" height="24" class="section-icon"></iconify-icon>
{{if eq .Lang "es"}}Competencias{{else}}Skills{{end}}
</h3>
</summary>
<p class="summary-text">
{{if eq .Lang "es"}}
Desarrollador <strong>full-stack</strong> con experiencia en <strong>Go</strong>, <strong>Node.js</strong>, <strong>React</strong> y <strong>HTMX</strong> para <strong>aplicaciones modernas</strong>, además de conocimientos en Java y PHP para proyectos legacy. He trabajado en <strong>unos 20 sitios web</strong> y realizado <strong>consultoría para 35-40 clientes internacionales</strong>, desde e-commerce y plataformas empresariales hasta <strong>sistemas de autenticación</strong> que gestionan <strong>millones de usuarios</strong>. Familiarizado con flujos de trabajo asistidos por <strong>IA</strong> y gestión de infraestructura (<strong>Linux</strong>, <strong>Docker</strong>, <strong>CI/CD</strong>). Me adapto bien tanto al trabajo independiente como colaborativo en equipos internacionales.
{{else}}
<strong>Full-stack</strong> developer with experience in <strong>Go</strong>, <strong>Node.js</strong>, <strong>React</strong>, and <strong>HTMX</strong> for <strong>modern applications</strong>, plus Java and PHP knowledge for legacy projects. I've worked on <strong>around 20 websites</strong> and provided <strong>consulting for 35-40 international clients</strong>, from e-commerce and enterprise platforms to <strong>authentication systems</strong> managing <strong>millions of users</strong>. Familiar with <strong>AI-assisted development</strong> workflows and infrastructure management (<strong>Linux</strong>, <strong>Docker</strong>, <strong>CI/CD</strong>). I adapt well to both independent work and collaborative teams across different countries.
{{end}}
</p>
</details>
</div>
<!-- Skeleton Content -->
<div class="skeleton-content">
<div class="skeleton-section">
<div class="skeleton skeleton-section-title"></div>
<div class="skeleton skeleton-summary-paragraph"></div>
<div class="skeleton skeleton-summary-paragraph" style="width: 95%;"></div>
<div class="skeleton skeleton-summary-paragraph" style="width: 90%;"></div>
<div class="skeleton skeleton-summary-paragraph" style="width: 85%;"></div>
<div class="skeleton skeleton-summary-paragraph" style="width: 92%;"></div>
</div>
</div>
</section>
{{end}}
+18 -2
View File
@@ -292,8 +292,8 @@ When adding tests:
**Philosophy**: Zero redundancy - Every test is essential and unique
### 12-skeleton-language-transitions.test.mjs
**Purpose**: Skeleton loader animations during language transitions
- ✅ Component wrapper structure (dual-state: actual + skeleton content)
**Purpose**: Skeleton loader animations during language transitions for ALL 13 curriculum sections
- ✅ Component wrapper structure (dual-state: actual + skeleton content) - **13 sections total**
- ✅ Skeleton CSS loaded (shimmer animation verified)
- ✅ First language switch (EN → ES) - Loading class added/removed
- ✅ Second language switch (ES → EN) - Consistent behavior
@@ -301,9 +301,25 @@ When adding tests:
- ✅ No stuck loading states (all containers clean after transition)
- ✅ JavaScript event handlers configured (languageSwitching flag)
**Sections with skeleton loaders (structural fidelity)**:
1. Header (title-badges + personal info)
2. Education
3. Skills Summary
4. Experience (with company logos, descriptions, responsibilities)
5. Awards (with logos, issuers, descriptions)
6. Projects (with icons, descriptions, tech stacks)
7. Courses (with icons, institutions, dates)
8. Languages
9. References
10. Other Information
11. Skills Sidebar (left) - Technical Skills
12. Skills Sidebar (right) - More Skills
13. Footer
**Implementation**: JavaScript event handlers in `static/js/main.js`
- `htmx:beforeRequest` - Adds `.loading` class to page containers
- `htmx:afterSettle` - Removes `.loading` class after swap completes (100ms delay)
- **Structural fidelity**: Each skeleton mirrors the exact structure of its actual content (logos, text lines, lists)
**Critical**: Migrated from hyperscript to JavaScript for reliable Playwright testing