**Main Changes:** 1. **Package Restructuring** - Separated mixed concerns into focused packages: - Created `internal/models/cv/` for CV domain logic (CV, Personal, Experience, etc.) - Created `internal/models/ui/` for UI presentation logic (InfoModal, ShortcutsModal, etc.) - Removed monolithic `internal/models/cv.go` (300+ lines → organized packages) 2. **Testing** - Added comprehensive unit tests: - `internal/models/cv/loader_test.go` - CV data loading and validation - `internal/models/ui/loader_test.go` - UI translations loading - All tests passing ✅ 3. **Documentation** - Added Go learning knowledge base: - `_go-learning/architecture/server-design.md` - Goroutines, graceful shutdown explained - `_go-learning/refactorings/001-cv-model-separation.md` - This refactoring documented - Public documentation showcasing Go expertise (README.md kept private) 4. **Handler Updates** - Updated imports to use new package structure: - `internal/handlers/cv.go` - Uses `cvmodel` and `uimodel` aliases **Benefits:** - ✅ Clear separation of concerns (domain vs presentation) - ✅ Better testability (isolated package testing) - ✅ Improved maintainability (smaller, focused files) - ✅ Scalability (each domain can evolve independently) - ✅ Follows Go best practices (small, cohesive packages) **Other Updates:** - Updated middleware security checks - Template improvements - Organized completed prompts **Testing:** - All Go unit tests pass (cv, ui, handlers) - Server verified with curl tests (English, Spanish, Health endpoints) - Frontend functionality unchanged (refactoring is transparent to UI)
15 KiB
Implement Skeleton Loader Transitions for Language Switching
Implement professional skeleton loader animations when switching between English and Spanish languages in the CV application. The goal is to create a polished, modern user experience similar to FriendKit (reference screenshots provided) where content transitions through skeleton/placeholder states with pulsing animations.Visual Reference: The user provided screenshots showing gray pulsing placeholder boxes that appear during page transitions - these skeleton loaders make the transition feel smooth and intentional while new content loads.
This will transform the current instant language switch into a premium, modern web app experience.
**Current Implementation:** - Language switching uses HTMX with `/switch-language?lang={en|es}` endpoint - HTMX performs out-of-band swaps (`hx-swap-oob="innerHTML"`) to update: - `#language-selector` (primary target) - `#cv-inner-content-page-1` - `#cv-inner-content-page-2` - Currently uses `hx-swap="outerHTML"` which causes instant, jarring content replacement - No intermediate loading state - content just "pops" from one language to anotherReference Files: @templates/language-switch.html - Server response template with OOB swaps @templates/partials/navigation/language-selector.html - Language selector buttons @static/css/main.css - Existing CSS with some transitions already in place
Tech Stack:
- HTMX for dynamic content swapping
- Hyperscript for custom behaviors (already in use)
- Vanilla CSS for animations (no external animation libraries)
- Go templates for server-side rendering
Desired User Experience (from screenshots):
- User clicks language button (EN or ES)
- Current content fades out (200-300ms)
- Skeleton loaders appear - gray pulsing boxes matching INSIDE of the CV (so, a toggle of the content)
- New language content loads from server
- Skeleton loaders fade out and new content fades in (200-300ms)
- Total smooth, professional transition feel
Why Skeleton Loaders Matter:
- Perceived performance: Users perceive loading as faster when they see progressive feedback
- Modern UX standard: Used by Facebook, LinkedIn, YouTube, and all modern web apps
- Reduces cognitive load: Shows users "something is happening" vs blank screen or instant swap
- Professional polish: Demonstrates attention to detail and quality
-
Three-Phase Transition:
- Phase 1 (Fade Out): Current content fades to opacity 0 (250ms)
- Phase 2 (Skeleton Display): Show skeleton loaders with pulse animation
- Phase 3 (Fade In): New content fades from opacity 0 to 1 (250ms)
- Total transition: ~500-700ms (feels premium, not sluggish)
-
HTMX Integration:
- Use HTMX swap timing modifiers:
swap:250ms settle:250ms - Leverage HTMX request lifecycle events for skeleton state management
- Use
htmx:beforeRequestto show skeleton - Use
htmx:afterSwapto hide skeleton and fade in content
- Use HTMX swap timing modifiers:
-
CSS Animations:
- Create
.skeleton-loadercomponent classes - Pulsing animation using CSS
@keyframes(opacity or background gradient shift) - Animation should be subtle: 1.5s-2s loop, infinite
- GPU-accelerated properties only (opacity, transform)
- Create
-
Layout Matching:
- Skeleton should mirror the actual CV layout structure
- Consider creating skeleton variants for:
- Header section (name, title, badges)
- Skills sidebar sections
- Experience entries
- Education entries
- Doesn't need to be pixel-perfect, just recognizable structure
-
Performance:
- Skeleton rendering should be instant (<16ms)
- Animations GPU-accelerated for smooth 60fps
- No layout thrashing during transitions
- Total transition under 700ms
-
Accessibility:
- Respect
prefers-reduced-motion- disable pulse animation - Add
aria-busy="true"during loading states - Ensure screen readers announce loading state
- Respect
-
Consistency:
- Use existing project timing patterns (0.2s-0.3s)
- Match existing design language and color palette
- Work on mobile and desktop viewports
Step 1: Add Skeleton States to Existing Components
Modify existing CV component templates to include built-in skeleton states:
templates/partials/cv-header.html- Add skeleton state markup inside the componenttemplates/partials/skills-*.html- Add skeleton placeholders for skill blockstemplates/partials/experience-entry.html- Add skeleton placeholders for experience blocks- Each component should have both actual content and skeleton markup, toggled via CSS classes
Example structure:
<div class="component-wrapper">
<div class="actual-content"><!-- Real CV content --></div>
<div class="skeleton-content hidden"><!-- Skeleton placeholders --></div>
</div>
Step 2: Create Skeleton CSS Animations
Add to static/css/main.css or create static/css/skeleton.css:
/* Skeleton loader base styles */
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-pulse 1.5s ease-in-out infinite;
border-radius: 4px;
}
@keyframes skeleton-pulse {
0%, 100% { background-position: 200% 0; }
50% { background-position: 0 0; }
}
/* Component state toggling */
.component-wrapper .actual-content {
opacity: 1;
transition: opacity 250ms ease;
}
.component-wrapper .skeleton-content {
opacity: 0;
position: absolute;
top: 0;
left: 0;
right: 0;
pointer-events: none;
transition: opacity 250ms ease;
}
/* Loading state - toggle content visibility */
.component-wrapper.loading .actual-content {
opacity: 0;
pointer-events: none;
}
.component-wrapper.loading .skeleton-content {
opacity: 1;
pointer-events: all;
}
/* Skeleton shapes - create boxes matching your layout */
.skeleton-header { height: 120px; margin-bottom: 20px; }
.skeleton-sidebar-item { height: 60px; margin-bottom: 10px; }
.skeleton-content-item { height: 100px; margin-bottom: 15px; }
/* Reduce motion support */
@media (prefers-reduced-motion: reduce) {
.skeleton { animation: none; background: #e0e0e0; }
}
Step 3: Update Language Selector with HTMX Timing
Modify templates/partials/navigation/language-selector.html:
- Add
hx-swap="outerHTML swap:250ms settle:250ms" - Add hyperscript to toggle
.loadingclass on component wrappers - Coordinate timing with component fade-in/out transitions
Step 4: Ensure Component Wrappers Have Proper Classes
Verify that CV component wrappers in templates have:
.component-wrapperclass for CSS targeting- Both
.actual-contentand.skeleton-contentchildren - Proper positioning context (relative positioning on wrapper)
Step 5: HTMX Event Handling with Hyperscript
Add hyperscript behavior to toggle loading state on components:
on htmx:beforeRequest from #language-selector
add .loading to .component-wrapper in #cv-inner-content-page-1
add .loading to .component-wrapper in #cv-inner-content-page-2
end
on htmx:afterSwap from #language-selector
wait 100ms -- Brief delay to ensure content is rendered
remove .loading from .component-wrapper in #cv-inner-content-page-1
remove .loading from .component-wrapper in #cv-inner-content-page-2
end
Alternative Approach (CSS-based): Use HTMX's automatic class management:
- Add
.component-wrapperto elements receiving OOB swaps - Use HTMX classes:
.htmx-swappingtriggers skeleton state - CSS:
.component-wrapper.htmx-swapping .actual-content { opacity: 0; } - CSS:
.component-wrapper.htmx-swapping .skeleton-content { opacity: 1; }
Step 6: Content Fade Transitions
Add fade-in/out to actual content sections:
.cv-page-content-wrapper {
transition: opacity 250ms ease;
}
.cv-page-content-wrapper.htmx-swapping {
opacity: 0;
}
.cv-page-content-wrapper.htmx-settling {
opacity: 1;
}
What to Prioritize:
- Add skeleton markup inside each existing component (dual-state structure)
- Implement CSS toggling between actual content and skeleton content
- Add pulsing animation to skeleton elements
- Refine skeleton shapes to better match layout
- Polish timing and transitions
- Add accessibility features
What to Avoid:
- Don't create a separate overlay layer - skeleton states live inside components
- Don't create overly complex skeleton markup - simple boxes are fine
- Don't make the skeleton identical to real content - approximate is better
- Don't use JavaScript animations - stick to CSS for performance
- Don't make transitions too long - 500-700ms total maximum
- Don't forget to test rapid clicking (skeleton should handle interruptions gracefully)
Why These Constraints Matter:
- Simplicity: Complex skeletons are harder to maintain when layout changes
- Performance: CSS animations are GPU-accelerated; JS animations cause jank
- UX Research: 400-700ms is the sweet spot - longer feels broken, shorter feels pointless
- Resilience: Users click fast - implementation must handle interruptions without breaking
-
Modify existing component templates (UPDATE)
./templates/partials/cv-header.html- Add skeleton state inside component./templates/partials/skills-*.html- Add skeleton placeholders./templates/partials/experience-entry.html- Add skeleton placeholders- Each component gets dual-state structure (actual content + skeleton content)
-
./static/css/skeleton.css(NEW)- Skeleton loader styles
- Pulsing animation keyframes
- Component state toggling styles (
.loadingclass behavior) - Responsive skeleton layouts
- Accessibility overrides for
prefers-reduced-motion
-
./templates/partials/navigation/language-selector.html- Add HTMX swap timing modifiers
- Add hyperscript to toggle
.loadingclass on component wrappers - Coordinate skeleton state transitions with HTMX events
-
./templates/language-switch.html- May need to coordinate OOB swap timing
- Ensure skeleton works with all content updates
-
./static/css/main.css- Add content fade-in/out transitions
- Import skeleton.css if created separately
- Add HTMX animation class styles
-
Ensure component wrappers are properly structured
- Each component should have
.component-wrapperclass - Components contain both
.actual-contentand.skeleton-content - Skeleton content is hidden by default, shown when
.loadingclass is added
- Each component should have
1. Visual Verification:
- Click EN button: Content fades out → Skeleton appears inside components → New content fades in
- Click ES button: Same smooth transition with skeleton
- Skeleton boxes pulse/shimmer smoothly inside each component
- Skeleton roughly matches CV layout (recognizable structure)
- Each component toggles between actual content and skeleton state
- No flashing or jarring jumps during transition
2. Timing Verification:
- Total transition feels responsive (<700ms)
- Skeleton appears immediately when language button clicked
- Content fade-out is smooth (not instant)
- Content fade-in is smooth after skeleton disappears
3. Interaction Testing:
- Rapidly click between EN and ES - no broken states
- Click language button while skeleton is showing - handles gracefully
- Mobile viewport - skeleton works correctly
- Desktop viewport - skeleton works correctly
4. Performance Testing:
- Open DevTools Performance tab
- Record during language switch
- Verify 60fps animation (no dropped frames)
- No layout thrashing or long tasks
- GPU acceleration active for animations
5. Accessibility Testing:
- Enable "Reduce motion" in OS settings → Skeleton pulse disabled
- Screen reader announces loading state
- Keyboard navigation still works during transitions
- Focus management doesn't break
6. Browser Compatibility:
- Chrome: Smooth skeleton animations
- Firefox: Smooth skeleton animations
- Safari: Smooth skeleton animations
- Mobile browsers: Touch interactions work correctly
7. Edge Cases:
- Slow network simulation (throttle to 3G) - skeleton visible longer
- Fast network - skeleton doesn't flash too quickly (minimum display time?)
- First load vs subsequent switches - consistent behavior
- Page refresh during skeleton display - recovers gracefully
Success Indicators: ✅ Components toggle between actual content and skeleton states during language transitions ✅ Skeleton states appear INSIDE each component (not as separate overlay) ✅ Pulsing animation is smooth and subtle ✅ Total transition time feels professional (500-700ms) ✅ No jarring content jumps or flashes ✅ Works consistently across browsers and devices ✅ Respects accessibility preferences ✅ Handles rapid interactions gracefully
<success_criteria>
- Each CV component toggles between actual content and skeleton state during language switching
- Skeleton loaders with pulsing animation appear INSIDE components (not as separate overlay)
- Three-phase transition: fade-out → skeleton → fade-in
- Total transition time: 500-700ms (feels premium, not sluggish)
- Skeleton structure roughly matches CV layout (recognizable)
- Animations are GPU-accelerated and buttery smooth (60fps)
- Respects
prefers-reduced-motionaccessibility setting - Handles rapid clicking without breaking or visual glitches
- Works consistently across modern browsers and device sizes
- Code follows existing project patterns and conventions
- Implementation is maintainable and doesn't overcomplicate the codebase </success_criteria>
Skeleton Loader Patterns:
- Facebook-style skeleton screens
- Modern progressive loading UX patterns
- LinkedIn content placeholders
Visual Reference (provided by user):
- FriendKit example pages showing gray pulsing placeholder boxes
- Boxes animate with subtle shimmer/pulse effect
- Content appears progressively after skeleton state
<additional_context> If you can provide the FriendKit code: If you have access to the actual CSS/HTML from the FriendKit examples, examining their skeleton loader implementation would be valuable for:
- Exact timing values they use
- Gradient/animation patterns for the pulse effect
- How they structure skeleton markup
- Any clever performance optimizations
However, we can implement an excellent skeleton loader without that code - the visual reference is sufficient to create a premium experience. </additional_context>