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
+296 -32
View File
@@ -1,36 +1,29 @@
/**
* Skeleton Loader for Language Transitions
* ==========================================
* Simple skeleton animation shown during HTMX content swaps
* Uses HTMX's built-in .htmx-swapping class
* Component-Level Skeleton Loaders for Language Transitions
* ==========================================================
* Each CV component has dual-state structure:
* - .actual-content (real CV content)
* - .skeleton-content (gray pulsing placeholders)
*
* Loading state controlled via .loading class on component wrapper
*/
/* ========================================================================
CONTENT FADE & SKELETON TRANSITION
BASE SKELETON STYLES
======================================================================== */
/* Fade out content when HTMX starts swapping */
.cv-page-content-wrapper.htmx-swapping {
opacity: 0.3;
transition: opacity 250ms ease;
position: relative;
}
/* Skeleton overlay appears during swap */
.cv-page-content-wrapper.htmx-swapping::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 0%,
#e8e8e8 20%,
#f0f0f0 40%,
#f0f0f0 100%
);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
animation: skeleton-shimmer 1.8s ease-in-out infinite;
border-radius: 4px;
pointer-events: none;
z-index: 1;
will-change: background-position;
}
@keyframes skeleton-shimmer {
@@ -42,23 +35,294 @@
}
}
/* Fade in new content */
.cv-page-content-wrapper.htmx-settling {
/* ========================================================================
COMPONENT WRAPPER STATE TOGGLING
======================================================================== */
/* Default state: Show actual content, hide skeleton */
.component-wrapper {
position: relative;
}
.component-wrapper .actual-content {
opacity: 1;
transition: opacity 250ms ease;
transition: opacity 250ms ease-out;
}
.component-wrapper .skeleton-content {
position: absolute;
top: 0;
left: 0;
right: 0;
opacity: 0;
pointer-events: none;
transition: opacity 250ms ease-out;
}
/* Loading state: Hide actual content, show skeleton */
.component-wrapper.loading .actual-content {
opacity: 0;
pointer-events: none;
}
.component-wrapper.loading .skeleton-content {
opacity: 1;
pointer-events: all;
}
/* ========================================================================
ACCESSIBILITY
SKELETON SHAPE DEFINITIONS
======================================================================== */
/* Header Section Skeleton */
.skeleton-header {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.skeleton-header-text {
flex: 1;
}
.skeleton-name {
height: 32px;
width: 70%;
margin-bottom: 8px;
}
.skeleton-experience-years {
height: 20px;
width: 50%;
margin-bottom: 16px;
}
.skeleton-photo {
width: 120px;
height: 120px;
border-radius: 50%;
flex-shrink: 0;
}
.skeleton-intro {
height: 60px;
width: 100%;
margin-top: 12px;
}
/* Section Title Skeleton */
.skeleton-section-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.skeleton-icon {
width: 24px;
height: 24px;
border-radius: 4px;
flex-shrink: 0;
}
.skeleton-title-text {
height: 24px;
width: 40%;
}
/* Skill Item Skeleton (Sidebar) */
.skeleton-skill-category {
margin-bottom: 20px;
}
.skeleton-skill-title {
height: 20px;
width: 60%;
margin-bottom: 12px;
}
.skeleton-skill-items {
display: flex;
flex-direction: column;
gap: 8px;
}
.skeleton-skill-item {
height: 32px;
width: 100%;
}
.skeleton-skill-item:nth-child(2) {
width: 85%;
}
.skeleton-skill-item:nth-child(3) {
width: 90%;
}
.skeleton-skill-item:nth-child(4) {
width: 75%;
}
/* Experience Entry Skeleton */
.skeleton-experience-item {
display: flex;
gap: 16px;
margin-bottom: 24px;
}
.skeleton-company-logo {
width: 60px;
height: 60px;
border-radius: 8px;
flex-shrink: 0;
}
.skeleton-experience-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.skeleton-position {
height: 20px;
width: 80%;
}
.skeleton-company-info {
height: 16px;
width: 60%;
}
.skeleton-description {
height: 40px;
width: 100%;
margin-top: 4px;
}
.skeleton-description.short {
width: 85%;
}
/* Education Item Skeleton */
.skeleton-education-item {
margin-bottom: 16px;
}
.skeleton-degree {
height: 18px;
width: 75%;
margin-bottom: 6px;
}
.skeleton-institution {
height: 16px;
width: 50%;
}
/* Text Block Skeletons (Generic) */
.skeleton-text {
height: 16px;
margin-bottom: 8px;
}
.skeleton-text.short {
width: 60%;
}
.skeleton-text.medium {
width: 80%;
}
.skeleton-text.long {
width: 95%;
}
/* ========================================================================
RESPONSIVE ADJUSTMENTS
======================================================================== */
@media (max-width: 768px) {
.skeleton-header {
flex-direction: column;
align-items: center;
}
.skeleton-header-text {
text-align: center;
width: 100%;
}
.skeleton-name,
.skeleton-experience-years {
width: 80%;
margin-left: auto;
margin-right: auto;
}
.skeleton-photo {
width: 100px;
height: 100px;
}
.skeleton-experience-item {
flex-direction: column;
gap: 12px;
}
.skeleton-company-logo {
width: 50px;
height: 50px;
}
}
/* ========================================================================
ACCESSIBILITY - REDUCED MOTION
======================================================================== */
@media (prefers-reduced-motion: reduce) {
.cv-page-content-wrapper.htmx-swapping::before {
.skeleton {
animation: none;
background: #e8e8e8;
}
.cv-page-content-wrapper {
transition: none !important;
.component-wrapper .actual-content,
.component-wrapper .skeleton-content {
transition: none;
}
}
/* ========================================================================
PRINT STYLES
======================================================================== */
@media print {
.skeleton-content {
display: none !important;
}
.component-wrapper .actual-content {
opacity: 1 !important;
}
}
/* ========================================================================
PERFORMANCE OPTIMIZATIONS
======================================================================== */
/* Force GPU acceleration for skeleton elements */
.skeleton {
transform: translateZ(0);
backface-visibility: hidden;
}
/* Contain layout/paint/style to prevent reflow */
.component-wrapper {
contain: layout style;
}
/* Optimize skeleton rendering */
.skeleton-content {
contain: layout paint;
}