docs: Phase 2 - Add Skeleton Loaders and Color Theme System sections
MAJOR FEATURES DOCUMENTED: ### Section 11: Skeleton Loaders (lines 1638-1764) Component-level content placeholders for smooth loading transitions KEY FEATURES: - Pixel-perfect skeleton matching (header, photo, sections, experience) - GPU-accelerated shimmer animation (1.8s ease-in-out infinite) - Component-wrapper architecture (dual-state: actual + skeleton) - HTMX event integration (beforeSwap → show, afterSettle → hide) - Zero layout shift during transitions - Accessibility: Respects prefers-reduced-motion - Print-friendly: Skeletons hidden in @media print IMPLEMENTATION: - static/css/skeleton.css (341 lines) - templates/partials/skeleton-loader.html - HTMX event listeners in main.js - tests/mjs/12-skeleton-language-transitions.test.mjs BENEFITS: - Professional UX like modern SPAs (LinkedIn, Facebook) - Smooth 250ms fade transitions - Reusable for any HTMX swap operation - Independent component loading states ### Section 12: Color Theme System (lines 1767-1939) Dynamic light/dark/auto theme switching with CSS custom properties KEY FEATURES: - Three theme modes: Auto (system), Light, Dark - CSS custom properties for instant switching - localStorage persistence across sessions - System integration via prefers-color-scheme - Dynamic button colors per theme mode: * Auto: Purple (#9b59b6) * Light: Orange/Yellow (#f39c12) * Dark: Blue (#3498db) - Dual-theme architecture (Color + Layout independent) IMPLEMENTATION: - static/css/color-theme.css (258 lines) - static/hyperscript/color-theme._hs (59 lines) - setColorTheme(), initColorTheme() functions - Fixed floating button with theme cycling - tests/mjs/13-color-theme-switcher.test.mjs BENEFITS: - User comfort (choose preference or follow system) - Instant switching (CSS custom properties, no reflow) - Accessible (WCAG AA contrast compliance) - Zero JavaScript for theme application - Separation of concerns (color vs. layout) DOCUMENTATION QUALITY: - Complete before/after code examples - Architecture pattern breakdowns - Comprehensive benefits lists - Testing information included - Pixel-perfect matching tables (skeletons) - CSS custom properties reference (theme) IMPACT: +306 lines comprehensive documentation TIME: ~90 minutes (partial Phase 2 complete) REMAINING: HTMX patterns enhancement, hover sync documentation
This commit is contained in:
@@ -1635,6 +1635,310 @@ Time 600ms+: opacity=NaN ← Element destroyed!
|
||||
|
||||
---
|
||||
|
||||
### 11. Skeleton Loaders - Component-Level Content Placeholders
|
||||
|
||||
**Problem:** Language transitions caused jarring white screen flashes as new content loaded. Users experienced:
|
||||
- Abrupt blank states during HTMX swaps
|
||||
- No visual continuity during async operations
|
||||
- Unclear loading state
|
||||
- Layout shift after content loaded
|
||||
|
||||
**Solution:** Component-level skeleton loaders with pixel-perfect placeholder matching.
|
||||
|
||||
#### Before (Jarring Transitions):
|
||||
```html
|
||||
<!-- User clicks language toggle -->
|
||||
<!-- Screen goes blank while waiting for server -->
|
||||
<!-- Content suddenly appears -->
|
||||
<!-- User confused about what happened -->
|
||||
```
|
||||
|
||||
#### After (Smooth Skeleton Transitions):
|
||||
```html
|
||||
<!-- Component wrapper with dual states -->
|
||||
<div class="component-wrapper" data-component="header">
|
||||
<!-- Actual content (visible by default) -->
|
||||
<div class="actual-content">
|
||||
<h1>Juan Teodoro</h1>
|
||||
<p>15+ Years Full-Stack Experience</p>
|
||||
</div>
|
||||
|
||||
<!-- Skeleton content (hidden by default) -->
|
||||
<div class="skeleton-content">
|
||||
<div class="skeleton skeleton-name"></div>
|
||||
<div class="skeleton skeleton-experience-years"></div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```css
|
||||
/* static/css/skeleton.css - Component-level skeleton system */
|
||||
|
||||
/* Base skeleton with shimmer animation */
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #f0f0f0 0%, #e8e8e8 20%, #f0f0f0 40%, #f0f0f0 100%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-shimmer 1.8s ease-in-out infinite;
|
||||
border-radius: 4px;
|
||||
will-change: background-position;
|
||||
transform: translateZ(0); /* GPU acceleration */
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* Loading state toggle */
|
||||
.component-wrapper.loading .actual-content {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.component-wrapper.loading .skeleton-content {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Language switch with skeleton loading
|
||||
htmx.on('htmx:beforeSwap', function(evt) {
|
||||
// Show skeletons BEFORE swap
|
||||
document.querySelectorAll('.component-wrapper').forEach(wrapper => {
|
||||
wrapper.classList.add('loading');
|
||||
});
|
||||
});
|
||||
|
||||
htmx.on('htmx:afterSettle', function(evt) {
|
||||
// Hide skeletons AFTER content settles
|
||||
document.querySelectorAll('.component-wrapper').forEach(wrapper => {
|
||||
wrapper.classList.remove('loading');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Architecture Pattern:**
|
||||
1. **User clicks language toggle** → HTMX `beforeSwap` event fires
|
||||
2. **JavaScript adds `.loading` class** → Triggers CSS transition (actual-content opacity: 1 → 0, skeleton-content opacity: 0 → 1)
|
||||
3. **Skeleton appears** → Smooth 250ms fade-in with shimmer animation
|
||||
4. **HTMX fetches new language content** → Server renders and returns HTML
|
||||
5. **HTMX swaps content** → New actual-content replaces old
|
||||
6. **afterSettle event fires** → JavaScript removes `.loading` class
|
||||
7. **Skeleton fades out** → Smooth 250ms fade (skeleton opacity: 1 → 0, actual-content opacity: 0 → 1)
|
||||
8. **Result** → Smooth, professional loading experience with zero layout shift
|
||||
|
||||
**Benefits:**
|
||||
- ✅ **Zero layout shift** - Skeletons match exact dimensions of actual content
|
||||
- ✅ **Professional UX** - Smooth transitions like modern SPAs (LinkedIn, Facebook)
|
||||
- ✅ **Performance** - GPU-accelerated animations, CSS containment optimization
|
||||
- ✅ **Accessibility** - Respects `prefers-reduced-motion`, animations pause for users who need it
|
||||
- ✅ **Print-friendly** - Skeletons hidden in print CSS
|
||||
- ✅ **Maintainability** - Component-level structure, easy to add/modify skeletons
|
||||
- ✅ **Reusable** - Works for any HTMX swap operation, not just language switch
|
||||
|
||||
**Implementation Locations:**
|
||||
- **CSS:** `static/css/skeleton.css` (341 lines) - Complete skeleton system
|
||||
- **Template:** `templates/partials/skeleton-loader.html` - Skeleton placeholders
|
||||
- **Component wrappers:** Each CV section wrapped with `.component-wrapper`
|
||||
- **HTMX events:** `static/js/main.js` - beforeSwap/afterSettle listeners
|
||||
|
||||
**Testing:** Automated tests in `tests/mjs/12-skeleton-language-transitions.test.mjs` verify:
|
||||
- Skeletons appear during language switch
|
||||
- Content replaced without full page reload
|
||||
- Skeletons removed after content loads
|
||||
- Zero layout shift during transition
|
||||
|
||||
**Pixel-Perfect Matching:**
|
||||
|
||||
| Component | Skeleton Dimensions | Actual Content Match |
|
||||
|-----------|---------------------|----------------------|
|
||||
| Header name | 40px height, 75% width | `<h1>` tag exact size |
|
||||
| Experience years | 24px height, 55% width | Subtitle exact size |
|
||||
| Profile photo | 150x200px, absolute positioned | Photo exact dimensions |
|
||||
| Section titles | 24px height + icon gap | Title + iconify-icon |
|
||||
| Experience items | 60px logo + flex content | Logo + text layout |
|
||||
| Skill badges | 32px height pills | Actual skill badge size |
|
||||
|
||||
**Key Innovation:** Component-level architecture allows each CV section to independently show loading state without affecting rest of page. Skeletons are absolutely positioned overlays, so they don't disrupt document flow.
|
||||
|
||||
---
|
||||
|
||||
### 12. Color Theme System - Dynamic Light/Dark/Auto Switching
|
||||
|
||||
**Problem:** Users have different preferences for light vs. dark interfaces, and forced single theme causes:
|
||||
- Eye strain for users in low-light environments (forced light theme)
|
||||
- Poor contrast for users in bright environments (forced dark theme)
|
||||
- Inability to match system preferences
|
||||
- No user control over visual comfort
|
||||
|
||||
**Solution:** CSS custom properties with dynamic theme switching (auto/light/dark modes).
|
||||
|
||||
#### Before (Single Theme Only):
|
||||
```css
|
||||
/* Hard-coded light theme only */
|
||||
body {
|
||||
background: #ffffff;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
```
|
||||
|
||||
#### After (Dynamic Theme System):
|
||||
```css
|
||||
/* static/css/color-theme.css - CSS Custom Properties */
|
||||
|
||||
/* Light theme (default) */
|
||||
:root {
|
||||
--page-bg: #b8bbbe;
|
||||
--paper-bg: #ffffff;
|
||||
--text-primary: #1a1a1a;
|
||||
--text-secondary: #333333;
|
||||
--action-bar-bg: #2b2b2b;
|
||||
--shadow-lg: 2px 2px 9px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Dark theme override */
|
||||
[data-color-theme="dark"] {
|
||||
--page-bg: rgb(82, 86, 89);
|
||||
--paper-bg: #1a1a1a;
|
||||
--text-primary: #e0e0e0;
|
||||
--text-secondary: #d0d0d0;
|
||||
--action-bar-bg: #1a1a1a;
|
||||
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* Auto theme - follows system preference */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
[data-color-theme="auto"] {
|
||||
/* Same dark theme variables */
|
||||
--page-bg: rgb(82, 86, 89);
|
||||
--paper-bg: #1a1a1a;
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
|
||||
/* Components use custom properties */
|
||||
.cv-paper {
|
||||
background: var(--paper-bg);
|
||||
color: var(--text-primary);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
```
|
||||
|
||||
```hyperscript
|
||||
-- static/hyperscript/color-theme._hs
|
||||
|
||||
def setColorTheme(mode)
|
||||
-- Save preference to localStorage
|
||||
call localStorage.setItem('color-theme-mode', mode)
|
||||
|
||||
-- Apply theme to document
|
||||
call document.documentElement.setAttribute('data-color-theme', mode)
|
||||
|
||||
-- Update button icon based on mode
|
||||
if mode is 'light' then
|
||||
call document.querySelector('#themeIcon').setAttribute('icon', 'mdi:white-balance-sunny')
|
||||
end
|
||||
if mode is 'dark' then
|
||||
call document.querySelector('#themeIcon').setAttribute('icon', 'mdi:moon-waning-crescent')
|
||||
end
|
||||
if mode is 'auto' then
|
||||
call document.querySelector('#themeIcon').setAttribute('icon', 'mdi:theme-light-dark')
|
||||
end
|
||||
end
|
||||
|
||||
def initColorTheme()
|
||||
-- Get saved preference or default to 'auto'
|
||||
set savedTheme to localStorage['color-theme-mode'] or 'auto'
|
||||
call setColorTheme(savedTheme)
|
||||
end
|
||||
```
|
||||
|
||||
**Theme Switcher UI:**
|
||||
```html
|
||||
<!-- Fixed floating button -->
|
||||
<button class="color-theme-switcher"
|
||||
data-theme-mode="auto"
|
||||
_="on click
|
||||
set currentMode to localStorage['color-theme-mode'] or 'auto'
|
||||
if currentMode is 'auto' then call setColorTheme('light')
|
||||
else if currentMode is 'light' then call setColorTheme('dark')
|
||||
else call setColorTheme('auto')
|
||||
end">
|
||||
<iconify-icon id="themeIcon" icon="mdi:theme-light-dark"></iconify-icon>
|
||||
</button>
|
||||
```
|
||||
|
||||
```css
|
||||
/* Dynamic button colors based on active theme */
|
||||
.color-theme-switcher:hover[data-theme-mode="light"] {
|
||||
background: #f39c12 !important; /* Warm sun yellow */
|
||||
}
|
||||
|
||||
.color-theme-switcher:hover[data-theme-mode="dark"] {
|
||||
background: #3498db !important; /* Cool moon blue */
|
||||
}
|
||||
|
||||
.color-theme-switcher:hover[data-theme-mode="auto"] {
|
||||
background: #9b59b6 !important; /* Purple (mix of both) */
|
||||
}
|
||||
```
|
||||
|
||||
**Theme Cycle Sequence:**
|
||||
1. **Auto** (default) - Follows system preference via `prefers-color-scheme`
|
||||
2. **Light** - Force light theme regardless of system
|
||||
3. **Dark** - Force dark theme regardless of system
|
||||
4. **Back to Auto** - Return to system preference
|
||||
|
||||
**Architecture Pattern:**
|
||||
1. **Page loads** → `initColorTheme()` runs, reads localStorage
|
||||
2. **User clicks theme button** → Cycles to next mode (auto → light → dark → auto)
|
||||
3. **`setColorTheme(mode)` executes** → Updates `data-color-theme` attribute on `<html>`
|
||||
4. **CSS cascade triggers** → All `var(--custom-property)` values update instantly
|
||||
5. **localStorage saves preference** → Persists across page reloads
|
||||
6. **Button icon updates** → Visual feedback (sun/moon/auto icons)
|
||||
7. **Button hover color changes** → Dynamic based on active mode
|
||||
|
||||
**Benefits:**
|
||||
- ✅ **User comfort** - Choose preferred theme or follow system
|
||||
- ✅ **Instant switching** - CSS custom properties update without reflow
|
||||
- ✅ **Persistent** - localStorage saves preference across sessions
|
||||
- ✅ **Accessible** - High contrast in both modes, WCAG AA compliant
|
||||
- ✅ **System integration** - Auto mode respects OS/browser settings
|
||||
- ✅ **Visual feedback** - Dynamic button colors indicate active mode
|
||||
- ✅ **Separation of concerns** - Color theme independent from layout theme (.theme-clean)
|
||||
- ✅ **Performance** - Zero JavaScript for theme application (CSS handles it)
|
||||
|
||||
**Implementation Locations:**
|
||||
- **CSS:** `static/css/color-theme.css` - Theme variables and button styles
|
||||
- **Hyperscript:** `static/hyperscript/color-theme._hs` (59 lines) - Theme switching logic
|
||||
- **Button:** Fixed position floating button (left: 2rem, bottom: 14rem on desktop)
|
||||
- **Mobile:** Repositioned in bottom action bar (5-button layout)
|
||||
|
||||
**Testing:** Automated tests in `tests/mjs/13-color-theme-switcher.test.mjs` verify:
|
||||
- Theme cycling works (auto → light → dark → auto)
|
||||
- localStorage persistence across reloads
|
||||
- Button colors change per theme mode
|
||||
- Icons update correctly
|
||||
- `data-color-theme` attribute applied
|
||||
|
||||
**CSS Custom Properties Used:**
|
||||
|
||||
| Variable | Light Theme | Dark Theme | Purpose |
|
||||
|----------|-------------|------------|---------|
|
||||
| `--page-bg` | #b8bbbe (gray) | rgb(82,86,89) (darker gray) | Page background |
|
||||
| `--paper-bg` | #ffffff (white) | #1a1a1a (near-black) | CV paper background |
|
||||
| `--text-primary` | #1a1a1a (black) | #e0e0e0 (light gray) | Main text color |
|
||||
| `--action-bar-bg` | #2b2b2b (dark gray) | #1a1a1a (darker) | Top bar background |
|
||||
| `--shadow-lg` | 2px 2px 9px rgba(0,0,0,0.5) | 0 4px 16px rgba(0,0,0,0.6) | Paper shadow |
|
||||
|
||||
**Key Innovation:** Dual-theme system (Color + Layout) allows complete customization:
|
||||
- **Color theme** (this section): Light/Dark/Auto color palette
|
||||
- **Layout theme** (.theme-clean): Sidebar vs. clean layout structure
|
||||
- Both independent, can be combined (e.g., dark mode + clean layout)
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Phase 9: Zoom Control Bug Fixes (November 2025)
|
||||
|
||||
### Issue 1: X Button Not Working
|
||||
|
||||
Reference in New Issue
Block a user