feat: implement component-level skeleton loaders for language transitions

Implemented dual-state skeleton system as specified in prompt:
- Each component has actual-content + skeleton-content structure
- CSS toggles visibility via .loading class on component-wrapper
- Individual skeleton boxes (name, photo, intro, etc.) with shimmer animation
- Hyperscript triggers loading state during language switch

Changes:
- skeleton.css: Complete component-level skeleton CSS system with shimmer
- language-selector.html: Hyperscript to add/remove .loading class
- header.html: Dual-state structure with skeleton placeholders

Behavior:
- Click language button → .loading class added to components
- Actual content fades out (opacity → 0) in 250ms
- Skeleton boxes appear and shimmer
- After HTMX swap + 100ms delay → .loading class removed
- New content fades in (opacity → 1) in 250ms

Test Results:
 Component wrapper structure verified
 Dual-state toggle working correctly
 Skeleton elements present and animated
 Shimmer animation active (1.8s infinite loop)
 Accessibility: respects prefers-reduced-motion
 Print: skeletons hidden, content always visible

Next: Add skeleton structure to remaining components (experience, education, skills, projects)
This commit is contained in:
juanatsap
2025-11-18 11:32:12 +00:00
parent 7ea2e93fe1
commit 534e0f16e5
3 changed files with 331 additions and 50 deletions
@@ -28,11 +28,12 @@
hx-push-url="/?lang=en"
aria-label="English"
_="on htmx:beforeRequest
add .htmx-swapping to #cv-inner-content-page-1
add .htmx-swapping to #cv-inner-content-page-2
add .loading to .component-wrapper in #cv-inner-content-page-1
add .loading to .component-wrapper in #cv-inner-content-page-2
on htmx:afterSwap
remove .htmx-swapping from #cv-inner-content-page-1
remove .htmx-swapping from #cv-inner-content-page-2">
wait 100ms
remove .loading from .component-wrapper in #cv-inner-content-page-1
remove .loading from .component-wrapper in #cv-inner-content-page-2">
<span>English</span>
</button>
<button class="selector-btn {{if eq .Lang "es"}}active{{end}}"
@@ -44,11 +45,12 @@
hx-push-url="/?lang=es"
aria-label="Español"
_="on htmx:beforeRequest
add .htmx-swapping to #cv-inner-content-page-1
add .htmx-swapping to #cv-inner-content-page-2
add .loading to .component-wrapper in #cv-inner-content-page-1
add .loading to .component-wrapper in #cv-inner-content-page-2
on htmx:afterSwap
remove .htmx-swapping from #cv-inner-content-page-1
remove .htmx-swapping from #cv-inner-content-page-2">
wait 100ms
remove .loading from .component-wrapper in #cv-inner-content-page-1
remove .loading from .component-wrapper in #cv-inner-content-page-2">
<span>Español</span>
</button>
</div>
+25 -10
View File
@@ -1,18 +1,33 @@
{{define "section-header"}}
<!-- Header with Name and Photo -->
<div class="cv-header">
<div class="cv-header-content">
<div class="cv-header-left">
<h1 class="cv-name">Moreno Rubio, Juan Andrés</h1>
<p class="years-experience">{{.YearsOfExperience}} {{if eq .Lang "es"}}años de experiencia{{else}}years of experience{{end}}</p>
<div class="cv-header component-wrapper">
<!-- Actual Content -->
<div class="actual-content">
<div class="cv-header-content">
<div class="cv-header-left">
<h1 class="cv-name">Moreno Rubio, Juan Andrés</h1>
<p class="years-experience">{{.YearsOfExperience}} {{if eq .Lang "es"}}años de experiencia{{else}}years of experience{{end}}</p>
<!-- Photo positioned for mobile (centered between name and intro) -->
<div class="cv-photo">
<img src="/static/images/profile/dni.jpeg" alt="{{.CV.Personal.Name}}" onerror="this.src='/static/images/profile/placeholder.svg'">
<!-- Photo positioned for mobile (centered between name and intro) -->
<div class="cv-photo">
<img src="/static/images/profile/dni.jpeg" alt="{{.CV.Personal.Name}}" onerror="this.src='/static/images/profile/placeholder.svg'">
</div>
<!-- Intro/Excerpt Text - No section heading, just the text -->
<div class="intro-text">{{.CV.Summary}}</div>
</div>
</div>
</div>
<!-- Intro/Excerpt Text - No section heading, just the text -->
<div class="intro-text">{{.CV.Summary}}</div>
<!-- Skeleton Content -->
<div class="skeleton-content">
<div class="skeleton-header">
<div class="skeleton-header-text">
<div class="skeleton skeleton-name"></div>
<div class="skeleton skeleton-experience-years"></div>
<div class="skeleton skeleton-intro"></div>
</div>
<div class="skeleton skeleton-photo"></div>
</div>
</div>
</div>