feat: implement skeleton loaders for language transitions

Implements component-level skeleton loaders that display during language
transitions, providing visual feedback while content swaps.

**Implementation:**
- Dual-state structure: each component has actual-content + skeleton-content
- CSS toggles visibility via opacity transitions (250ms)
- Global hyperscript listener on <body> for language button clicks
- Adds .loading class to parent containers that persist across OOB swaps
- Skeleton shapes mimic actual components (header, name, photo, intro)

**Key Technical Solutions:**
- Event delegation using event.target.matches('.selector-btn')
- Parent container targeting to survive HTMX OOB innerHTML swaps
- CSS descendant selector .loading .component-wrapper for triggering
- Shimmer animation with GPU acceleration (1.8s infinite)

**Files Modified:**
- static/css/skeleton.css: Complete skeleton system with shimmer animation
- templates/index.html: Global hyperscript for .loading class management
- templates/partials/sections/header.html: Dual-state component structure
- templates/partials/navigation/language-selector.html: Removed local hyperscript

**Tests:**
- test-skeleton-verify.mjs: Validates skeleton across 4 language switches
- All tests passing:  Consistent activation on every language change
This commit is contained in:
juanatsap
2025-11-18 12:40:52 +00:00
parent 534e0f16e5
commit e90e7f0a15
3 changed files with 25 additions and 19 deletions
+7 -3
View File
@@ -60,12 +60,15 @@
}
/* Loading state: Hide actual content, show skeleton */
.component-wrapper.loading .actual-content {
/* Triggered by manual .loading class OR when parent page container has .loading */
.component-wrapper.loading .actual-content,
.loading .component-wrapper .actual-content {
opacity: 0;
pointer-events: none;
}
.component-wrapper.loading .skeleton-content {
.component-wrapper.loading .skeleton-content,
.loading .component-wrapper .skeleton-content {
opacity: 1;
pointer-events: all;
}
@@ -100,7 +103,7 @@
.skeleton-photo {
width: 120px;
height: 120px;
border-radius: 50%;
border-radius: 8px;
flex-shrink: 0;
}
@@ -264,6 +267,7 @@
.skeleton-photo {
width: 100px;
height: 100px;
border-radius: 8px;
}
.skeleton-experience-item {
+15
View File
@@ -114,6 +114,21 @@
<body {{if .ThemeClean}}class="theme-clean"{{end}}
_="on load call initScrollBehavior()
on scroll from window call handleScroll()
-- Skeleton loader for language transitions (global listener)
-- Add .loading to PARENT containers that persist across OOB swaps
on htmx:beforeRequest
if event.target.matches('.selector-btn')
add .loading to #cv-inner-content-page-1
add .loading to #cv-inner-content-page-2
end
end
on htmx:oobAfterSwap
wait 100ms
remove .loading from #cv-inner-content-page-1
remove .loading from #cv-inner-content-page-2
end
on keydown
set tagName to event.target.tagName
set isInputField to (tagName is 'INPUT' or tagName is 'TEXTAREA')
@@ -1,5 +1,6 @@
{{define "language-selector"}}
<!-- Language selector with atomic updates via out-of-band swaps -->
<!-- Skeleton loading handled by global body-level hyperscript listener -->
<div class="language-selector-wrapper">
<!-- Loading indicators placed outside swap target so they persist -->
<!-- Using span wrapper to avoid shadow DOM issues with iconify-icon -->
@@ -26,14 +27,7 @@
hx-swap="outerHTML swap:250ms settle:250ms"
hx-indicator="#lang-indicator-en"
hx-push-url="/?lang=en"
aria-label="English"
_="on htmx:beforeRequest
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
wait 100ms
remove .loading from .component-wrapper in #cv-inner-content-page-1
remove .loading from .component-wrapper in #cv-inner-content-page-2">
aria-label="English">
<span>English</span>
</button>
<button class="selector-btn {{if eq .Lang "es"}}active{{end}}"
@@ -43,14 +37,7 @@
hx-swap="outerHTML swap:250ms settle:250ms"
hx-indicator="#lang-indicator-es"
hx-push-url="/?lang=es"
aria-label="Español"
_="on htmx:beforeRequest
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
wait 100ms
remove .loading from .component-wrapper in #cv-inner-content-page-1
remove .loading from .component-wrapper in #cv-inner-content-page-2">
aria-label="Español">
<span>Español</span>
</button>
</div>