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)
|
## 🐛 Phase 9: Zoom Control Bug Fixes (November 2025)
|
||||||
|
|
||||||
### Issue 1: X Button Not Working
|
### Issue 1: X Button Not Working
|
||||||
|
|||||||
Reference in New Issue
Block a user