Add detailed, user-friendly documentation for PDF export feature in
CUSTOMIZATION.md to help template users understand and customize
PDF generation.
New Documentation:
- Clear explanation of how PDF export works
- All 4 parameters (lang, length, icons, version) with examples
- Filename convention and patterns
- Frontend integration examples (HTML, HTMX)
- Tips for best PDF results
- Troubleshooting common issues
- Advanced customization guidance
Audience:
- Template users who want to customize their own CV
- Non-technical users who need to understand PDF options
- Developers who want to extend PDF functionality
Examples include:
- Complete URL patterns for all parameter combinations
- Code snippets for frontend buttons
- CSS customizations for print appearance
- Common troubleshooting solutions
**Issue**: Color theme button colors were too similar to other buttons
- Light mode (sun): Orange #f39c12 - same as keyboard button
- Dark mode (moon): Blue #3498db - same as zoom button
**Fix**: Updated to more distinctive colors
Light mode (sun):
- Was: Orange #f39c12 (same as keyboard)
- Now: Gold/Bright Yellow #ffd700 (distinct sun color)
Dark mode (moon):
- Was: Bright Blue #3498db (same as zoom)
- Now: Dark Slate Blue #2c3e50 (darker "nighty" blue)
Auto mode:
- Kept: Purple #9b59b6 (already distinctive)
**Result**: Color theme button now has unique, semantic colors that
clearly represent sun (bright yellow) and moon (dark night blue) without
conflicting with other buttons.
Updated both hover and at-bottom states for consistency.
**Bug 1: Wrong at-bottom colors**
- All buttons were showing orange (#f39c12) when at bottom
- User wanted each button to use its own hover color
**Bug 2: Zoom button missing at-bottom state**
- Zoom/search button wasn't changing color at bottom
**Fix - Updated .at-bottom styles to match hover colors:**
Download PDF button:
- Was: Orange (#f39c12)
- Now: PDF Red (#cd6060) - matches hover
Print Friendly button:
- Was: Orange (#f39c12)
- Now: White background + Green icon (#27ae60) - matches hover
Shortcuts button:
- Kept: Orange (#f39c12) - already correct
Color Theme Switcher:
- Was: Uniform orange
- Now: Dynamic colors based on theme mode
- Light mode: Yellow (#f39c12)
- Dark mode: Blue (#3498db)
- Auto mode: Purple (#9b59b6)
Info button:
- Already correct: Green (#27ae60)
Back-to-top button:
- Already correct: Green (#27ae60)
Zoom button:
- Added: Blue (#3498db) - matches hover
- Added to scroll handler in utils._hs
**Result**: Each button now shows its characteristic color when page
is scrolled to bottom, providing consistent visual feedback that matches
the hover state.
Files changed:
- static/hyperscript/utils._hs: Added zoom button
- static/css/main.css: Updated 3 button colors
- static/css/color-theme.css: Dynamic theme colors
**Issue**: Only info and shortcuts buttons were showing orange color when
scrolled to bottom of page. Download PDF, Print Friendly, and Color Theme
buttons were missing this visual feedback.
**Fix**:
1. Added all buttons to at-bottom class logic in utils._hs:
- #download-button
- #print-friendly-button
- .color-theme-switcher
2. Added CSS .at-bottom styles for missing buttons:
- .download-btn.at-bottom
- .print-friendly-btn.at-bottom
- .color-theme-switcher.at-bottom
**Result**: All fixed buttons now show unified orange (#f39c12) background
when page is scrolled to bottom (within 50px), providing consistent visual
feedback across all controls.
Files changed:
- static/hyperscript/utils._hs: Added 3 buttons to scroll handler
- static/css/main.css: Added 2 .at-bottom styles
- static/css/color-theme.css: Added 1 .at-bottom style
**Bug 1: Bidirectional Hover Sync**
- Fixed hover sync to work in BOTH directions
- Hovering left buttons → highlights menu buttons (was working)
- Hovering menu buttons → highlights left buttons (NOW WORKING)
- Added .download-btn and .print-friendly-btn to syncPdfHover/syncPrintHover selectors
**Bug 2: Theme-Aware CV Text Colors**
- CV content text now adapts to light/dark themes
- Light theme: Dark text (#1a1a1a, #333333) for readability
- Dark theme: Light text (#e0e0e0, #d0d0d0) for readability
- Added theme-specific overrides for --text-dark and --text-gray variables
- Solution uses CSS variables (not hardcoded) for future flexibility
Changes:
- static/hyperscript/hover-sync._hs: Added left-side button selectors
- static/css/color-theme.css: Added legacy variable overrides for all themes
Validates button positioning and responsive behavior across all viewports:
Desktop (>900px):
- Left side buttons (download, print, shortcuts, info) vertically stacked
- Back-to-top button on right side (intentional design)
- Zoom button visible
- Different bottom values verify vertical stacking
Wide Mobile (483-900px):
- Horizontal layout at bottom center
- Back-to-top remains on right side
- Zoom button hidden
Narrow Mobile (<483px):
- Back-to-top moved UP (5.5rem) to avoid overlap
- Still positioned on right side
- Horizontal button layout maintained
Accessibility:
- All buttons present and clickable
- Proper visibility checks
This test caught and validates the recent fixes:
1. Back-to-top on RIGHT (not left) in all mobile viewports
2. Narrow mobile positioning to prevent button overlap
3. Consistent hover effects across all buttons
Test results: 4/4 passed
- Desktop layout verification
- Wide mobile responsive layout
- Narrow mobile overlap prevention
- Button accessibility validation
Corrected positioning - button now stays on RIGHT side, just moved UP:
- right: 1.5rem (stays on right)
- bottom: 5.5rem (moved higher to avoid overlap)
This prevents overlap with bottom button row while maintaining
right-side positioning as requested.
**Problem:**
On screens narrower than 483px, the horizontal button layout caused overlap
between back-to-top button and other buttons due to limited width.
**Solution:**
Added media query for max-width: 483px to vertically stack back-to-top button:
- Moved back-to-top to LEFT side (left: 1.5rem)
- Positioned ABOVE info button (bottom: 5.5rem)
- Maintains 4rem vertical spacing between buttons (same as desktop)
**Layout behavior:**
- Above 900px: All buttons vertical on left side (desktop)
- 483px - 900px: Buttons horizontal at bottom center + back-to-top at bottom-right
- Below 483px: Buttons horizontal at bottom center + back-to-top stacked above info button (left side)
This prevents button overlap on narrow mobile devices while maintaining
consistent spacing and positioning.
Files modified: static/css/main.css
Fixed two UI bugs in button styling:
**1. Back-to-top button position in mobile:**
- BEFORE: Button was included in flexbox layout but not positioned (defaulted to left)
- AFTER: Excluded from flexbox layout, positioned at bottom-right (1.5rem) - same as desktop
- Mobile flexbox layout now has 5 buttons (download, print, shortcuts, theme, info)
- Back-to-top stays independently positioned at bottom-right corner
**2. Consistent hover effects for all buttons:**
- BEFORE: Only info and shortcuts buttons had enhanced box-shadow on hover
- AFTER: All buttons (download, print, shortcuts, info, back-to-top) have consistent hover effects
- Added `box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4) !important` to mobile hover states
- Creates unified "edge glow" effect across all button hovers
**Technical changes:**
- Split mobile button reset into two groups (flexbox buttons vs back-to-top)
- Enhanced mobile hover effect with box-shadow for visual consistency
- Maintains desktop behavior (back-to-top at bottom-right: 2rem)
Files modified: static/css/main.css
ISSUE: Matomo analytics not loading in production
ROOT CAUSE:
- Matomo script had nonce="{{.CSPNonce}}" attribute
- Go backend doesn't generate CSPNonce template variable
- Empty nonce attribute caused CSP to block the script in production
FIX:
- Removed nonce attribute from Matomo script tag
- CSP header already includes 'unsafe-inline' so nonces not needed
- Script now loads correctly in both localhost and production
VERIFICATION:
- Matomo will now load and track pageviews in production
- Check browser console for _paq object
- Verify tracking in Matomo dashboard
Remove SKELETON-LOADER-DEBUG-STATUS.md (318 lines) - debugging document no longer needed after successful resolution.
Feature status:
- Implementation complete (JavaScript migration from hyperscript)
- All 7 automated tests passing
- Manual testing verified working
- Documented in 2-MODERN-WEB-TECHNIQUES.md Section 11
This debug log served its purpose during troubleshooting but is now redundant with:
- Production code documentation
MIGRATION SUMMARY:
- Moved skeleton loader logic from hyperscript to JavaScript (main.js)
- Changed from htmx:oobAfterSwap to htmx:afterSettle event
- Changed OOB swap from innerHTML to outerHTML for proper element replacement
- Added languageSwitching flag for state tracking
- Added 100ms delay after afterSettle for final render completion
DOCUMENTATION UPDATES:
- 2-MODERN-WEB-TECHNIQUES.md: Updated skeleton loader section with
Complete documentation of HTMX loading indicators implementation:
- Added comprehensive section matching style of other 9 techniques
- Documented external indicator pattern to avoid swap-target destruction
- Included root cause analysis of initial bug (indicators destroyed mid-animation)
- Detailed CSS implementation with GPU-accelerated animations
- Implementation locations: language selector, toggle controls, hamburger menu
- Benefits: zero JavaScript, automatic lifecycle, accessibility support
- Testing: automated tests in 4-htmx.test.mjs
Updated:
- Technique count: 8 → 10 major optimizations
- Final stats: now lists all 10 techniques
- Last updated date: 2025-11-18
Deleted two obsolete documentation files that are no longer needed:
- COLOR-THEME-IMPLEMENTATION.md (204 lines) - Feature complete and documented in code
- prompts/003-implement-htmx-indicators.md (427 lines) - Implementation prompt no longer relevant
These files served their purpose during development but are now redundant with:
- Inline code documentation
- TEST-SUMMARY.md for test coverage
- README.md for user-facing features
- Git history for implementation
Complete color theme system (light/dark/auto) with dynamic UI:
Features:
- Color theme switcher with auto/light/dark modes
- Dynamic button colors on hover (purple/yellow/blue per theme)
- localStorage persistence across sessions
- Proper button positioning (desktop and mobile)
- Mobile: 5-button layout with theme before info button
Fixes:
- CSP updated to allow jsDelivr CDN for iconify icons
- Button repositioning: Download PDF and Print Friendly at top
- Hover-only colors (not persistent)
- Mobile button order corrected
Files:
- static/css/color-theme.css - Theme system with CSS variables
- static/js/color-theme.js - Theme switching logic
- templates/partials/color-theme-switcher.html - Button component
- internal/middleware/security.go - CSP fix for jsDelivr
- tests/mjs/13-color-theme-switcher.test.mjs - Comprehensive test
- tests/TEST-SUMMARY.md - Updated test documentation
Updates skeleton header to exactly match the actual CV header layout with
photo absolutely positioned on the right side.
**Layout Changes:**
- Photo: 150x200px positioned absolutely (top: 15px, right: 15px)
- Name: 40px height (h1 size)
- Subtitle: 24px height (larger)
- Intro text: 90px height (3-4 lines)
- Header has padding-right: 185px to accommodate photo
- Removed flex layout, using absolute positioning like actual
**Visual Improvements:**
- Photo border: 3px solid #e8e8e8 (matches actual white border)
- No border-radius on photo (matches actual)
- Larger text blocks for better visual match
- Proper spacing between elements
**Tests:**
- test-skeleton-verify.mjs: ✅ All 4 language switches pass
- Skeleton displays correctly on every transition
Implements component-level skeleton loaders that display during language
transitions, providing visual feedback while content swaps.
**Implementation:**
- Dual-state structure: each component has actual-content + skeleton-content
- CSS toggles visibility via opacity transitions (250ms)
- Global hyperscript listener on <body> for language button clicks
- Adds .loading class to parent containers that persist across OOB swaps
- Skeleton shapes mimic actual components (header, name, photo, intro)
**Key Technical Solutions:**
- Event delegation using event.target.matches('.selector-btn')
- Parent container targeting to survive HTMX OOB innerHTML swaps
- CSS descendant selector .loading .component-wrapper for triggering
- Shimmer animation with GPU acceleration (1.8s infinite)
**Files Modified:**
- static/css/skeleton.css: Complete skeleton system with shimmer animation
- templates/index.html: Global hyperscript for .loading class management
- templates/partials/sections/header.html: Dual-state component structure
- templates/partials/navigation/language-selector.html: Removed local hyperscript
**Tests:**
- test-skeleton-verify.mjs: Validates skeleton across 4 language switches
- All tests passing: ✅ Consistent activation on every language change
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)
Previously, the .htmx-swapping class was only applied to the language
selector button target, not to the actual CV content containers that
were being swapped via hx-swap-oob.
Solution:
- Added hyperscript event handlers to language selector buttons
- On htmx:beforeRequest → add .htmx-swapping to both content wrappers
- On htmx:afterSwap → remove .htmx-swapping from both wrappers
Effect:
- Skeleton shimmer (opacity 0.3 + animated ::before overlay) now
appears on CV content during language switch (~500ms duration)
- Smooth transition with shimmer effect inside content area
- No external overlay layer - effect is within existing DOM structure
Verified with Playwright test showing:
✅ .htmx-swapping class applied during swap
✅ Opacity changes to 0.3
✅ ::before pseudo-element with shimmer animation active
The footer was being zoomed along with CV content because it was
inside the #zoom-wrapper div. This fix moves the footer outside the
zoom-wrapper so it remains at normal size regardless of zoom level.
Changes:
- Move footer template outside zoom-wrapper in index.html
- Add UI exclusion test (11-zoom-ui-exclusion.test.mjs)
Test coverage:
- Verify footer, action bar, and menu are outside zoom-wrapper (DOM)
- Verify UI elements remain unchanged when zooming to 200%
- Verify CV content properly zooms while UI stays fixed
Before: Footer inside zoom-wrapper (zoomed with content)
After: Footer outside zoom-wrapper (stays normal size)
User can now zoom up to 300% for better readability.
Changes:
- Update zoom slider max from 175 to 300
- Update aria-valuemax to 300
- Update display label to show 300
- Update README to reflect new range (25%-300%)
Tested with curl - verified max="300" in HTML output
Zoom level persistence was broken because hyperscript was setting the
container's value instead of the slider's value on page load.
Changes:
- Fix zoom-control.html line 10: set #zoom-slider's value (not 'my value')
- Add comprehensive zoom persistence test (10-zoom-persistence.test.mjs)
- Update cv-functions.js documentation to clarify hyperscript interop
- Add zoom control feature to README
Test results: 5/5 tests pass
- Zoom saves to localStorage when changed ✅
- Zoom restores correctly on page reload ✅
- Reset to 100% works and persists ✅
Architecture note:
- Hyperscript 'call' within _="" attributes requires global JS scope
- JavaScript wrappers bridge window exposure to hyperscript evaluate()
- Pattern: window.fn() → _hyperscript.evaluate('hyperscriptFn()')
Problem: Hover sync not working after migration to hyperscript
Root cause: Hyperscript 'call' command requires functions in global JavaScript scope
- Hyperscript def functions are NOT automatically exposed to window
- Templates use _="on mouseenter call syncPdfHover(true)"
- This syntax expects a JavaScript function
Solution: Thin JavaScript wrappers that delegate to hyperscript implementations
- Wrappers use _hyperscript.evaluate() API to call hyperscript defs
- Functions exposed to window.* for global access
- Implementation stays in hyperscript, wrappers just bridge the gap
Affected functions:
- toggleCVLength, toggleIcons, toggleTheme (toggles._hs)
- syncPdfHover, syncPrintHover, highlightZoomControl (hover-sync._hs)
Why test didn't catch this:
- Test 8 dispatches events programmatically in JavaScript
- This triggers hyperscript handlers directly
- Real browser hover calls JavaScript functions which were missing
CRITICAL BUG FIX: Hover states now sync between action bar and hamburger menu
Changes:
1. Added mouseenter/mouseleave handlers to menu PDF button
- templates/partials/navigation/hamburger-menu.html:178-181
- Added .menu-pdf-btn class for targeting
- Added hyperscript hover sync events
2. Updated syncPdfHover() function
- static/js/cv-functions.js:71-82
- Now selects both .pdf-btn and .menu-pdf-btn
- Both buttons get .pdf-hover-sync class on hover
3. Updated syncPrintHover() function
- static/js/cv-functions.js:88-99
- Now selects both .print-btn and .menu-print-btn
- Both buttons get .print-hover-sync class on hover
4. Added CSS for menu PDF button hover sync
- static/css/main.css:2690-2700
- .menu-pdf-btn.pdf-hover-sync styling (white bg, red icon)
- Matches action bar PDF button hover state
5. Created comprehensive hover sync test
- tests/mjs/8-hover-sync.test.mjs
- Tests all 4 hover scenarios (bar→menu, menu→bar for both buttons)
- Validates event handlers and CSS class application
- Manual verification instructions included
Behavior now correct:
✅ Hovering action bar PDF button highlights menu PDF button
✅ Hovering action bar Print button highlights menu Print button
✅ Hovering menu PDF button highlights action bar PDF button
✅ Hovering menu Print button highlights action bar Print button
Fixes documented bug from PROJECT-MEMORY.md Section 3.
Critical corrections based on user feedback:
1. Hyperscript Parser Limit (Section 2)
- Changed status to "NEEDS RETESTING" with latest hyperscript version
- 3 def statement limit may no longer exist after upgrade
- TODO: Test if limit still applies with current version
2. Toggle + Hover Synchronization (Section 3)
- Clarified toggle checkboxes WORK perfectly ✅
- Documented HOVER SYNC BUG for PDF/Print buttons ❌
- Action bar hover doesn't sync with menu button hover
- Added file references and bug details
- Test coverage gap identified
3. Real-Time Rendering + Persistence (Section 4)
- Corrected misconception: BOTH DOM and localStorage required
- Not mutually exclusive - need instant feedback AND persistence
- Added clear examples of correct vs incorrect implementation
4. Architecture - Tech Stack
- CORRECTED: Backend is Go (NOT Bun/Hono)
- Go 1.21+ with standard library net/http
- Bun is ONLY test runner, not the runtime
- Added complete frontend stack details
5. File Organization
- CORRECTED: Actual Go project structure
- main.go entry point (v1.1.0)
- internal/ package organization
- Complete template hierarchy
- Critical files list updated
Fixes hallucinated architecture information and provides accurate
technical foundation for future developers.
Changed skeleton loader implementation strategy from separate overlay to built-in component states:
ARCHITECTURE CHANGE:
- Before: Separate skeleton-loader.html overlay positioned absolutely over content
- After: Each component contains both actual content and skeleton state markup
- Skeleton states toggle via CSS classes (.loading) on component wrappers
- No separate overlay layer - skeleton lives inside each component
IMPLEMENTATION