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:
Vendored
+7
-3
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user