e90e7f0a15
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
46 lines
1.9 KiB
HTML
46 lines
1.9 KiB
HTML
{{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 -->
|
|
<span id="lang-indicator-en" class="htmx-indicator small">
|
|
<iconify-icon icon="mdi:loading"
|
|
class="spinning light"
|
|
width="14"
|
|
height="14"
|
|
aria-label="Loading"></iconify-icon>
|
|
</span>
|
|
<span id="lang-indicator-es" class="htmx-indicator small">
|
|
<iconify-icon icon="mdi:loading"
|
|
class="spinning light"
|
|
width="14"
|
|
height="14"
|
|
aria-label="Loading"></iconify-icon>
|
|
</span>
|
|
|
|
<div class="language-selector" id="language-selector">
|
|
<button class="selector-btn {{if eq .Lang "en"}}active{{end}}"
|
|
data-short="EN"
|
|
hx-get="/switch-language?lang=en"
|
|
hx-target="#language-selector"
|
|
hx-swap="outerHTML swap:250ms settle:250ms"
|
|
hx-indicator="#lang-indicator-en"
|
|
hx-push-url="/?lang=en"
|
|
aria-label="English">
|
|
<span>English</span>
|
|
</button>
|
|
<button class="selector-btn {{if eq .Lang "es"}}active{{end}}"
|
|
data-short="ES"
|
|
hx-get="/switch-language?lang=es"
|
|
hx-target="#language-selector"
|
|
hx-swap="outerHTML swap:250ms settle:250ms"
|
|
hx-indicator="#lang-indicator-es"
|
|
hx-push-url="/?lang=es"
|
|
aria-label="Español">
|
|
<span>Español</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{{end}}
|