From 534e0f16e519eb4696d81f3be278e8c0968ae73a Mon Sep 17 00:00:00 2001 From: juanatsap Date: Tue, 18 Nov 2025 11:32:12 +0000 Subject: [PATCH] feat: implement component-level skeleton loaders for language transitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- static/css/skeleton.css | 328 ++++++++++++++++-- .../navigation/language-selector.html | 18 +- templates/partials/sections/header.html | 35 +- 3 files changed, 331 insertions(+), 50 deletions(-) diff --git a/static/css/skeleton.css b/static/css/skeleton.css index 7dbaa5e..5924088 100644 --- a/static/css/skeleton.css +++ b/static/css/skeleton.css @@ -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; +} diff --git a/templates/partials/navigation/language-selector.html b/templates/partials/navigation/language-selector.html index 9011c32..377182c 100644 --- a/templates/partials/navigation/language-selector.html +++ b/templates/partials/navigation/language-selector.html @@ -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"> English diff --git a/templates/partials/sections/header.html b/templates/partials/sections/header.html index 474d1f2..766a0fc 100644 --- a/templates/partials/sections/header.html +++ b/templates/partials/sections/header.html @@ -1,18 +1,33 @@ {{define "section-header"}} -
-
-
-

Moreno Rubio, Juan Andrés

-

{{.YearsOfExperience}} {{if eq .Lang "es"}}años de experiencia{{else}}years of experience{{end}}

+
+ +
+
+
+

Moreno Rubio, Juan Andrés

+

{{.YearsOfExperience}} {{if eq .Lang "es"}}años de experiencia{{else}}years of experience{{end}}

- -
- {{.CV.Personal.Name}} + +
+ {{.CV.Personal.Name}} +
+ + +
{{.CV.Summary}}
+
+
- -
{{.CV.Summary}}
+ +
+
+
+
+
+
+
+