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
CRITICAL FIX: Icon toggle now works without page refresh
- Changed class name from 'show-logos' to 'show-icons' (CSS mismatch bug)
- Updated localStorage key from 'cv-logos' to 'cv-icons'
- Fixed toggleIcons() function in cv-functions.js
HYPERSCRIPT ARCHITECTURE:
- Moved 6 toggle functions from hyperscript to JavaScript (cv-functions.js)
- Solves hyperscript 0.9.14 parser limitation (max 3 def statements total)
- Upgraded hyperscript from 0.9.12 to 0.9.14
- Fixed operator precedence in keyboard shortcuts
- Cleaned view-controls.html templates (inline → function calls)
NEW FILES:
- static/js/cv-functions.js - Global toggle functions (6 functions)
- HYPERSCRIPT-RULES.md - Permanent architecture documentation
- tests/mjs/0-zoom.test.mjs - Zoom functionality test
- tests/mjs/1-toggles.test.mjs - Comprehensive toggle test with real-time verification
- tests/TEST-SUMMARY.md - Test suite documentation
TESTS:
- Real-time DOM update verification (no refresh required)
- Screenshot capture for visual regression
- localStorage persistence validation
- Toggle synchronization between action bar and menu
BREAKING CHANGE: localStorage key changed from 'cv-logos' to 'cv-icons'
Users may need to re-toggle icons preference on first load after update.
**Problem**: Toggles in action bar and hamburger menu were not
synchronizing. When user clicked one toggle, the other didn't update.
**Root Cause**: After restoring toggle functions, they were setting BOTH
checkboxes explicitly instead of using the hyperscript 'or' operator
pattern from the working version.
**Fix**: Restored the original pattern from commit d4ef91b:
- Use `set otherToggle to (#lengthToggle or #lengthToggleMenu)`
- This dynamically selects "the other" toggle (not the one being clicked)
- Simplified code: removed redundant null checks for both checkboxes
- Fixed all 3 toggles: toggleCVLength, toggleIcons, toggleTheme
**Benefits**:
- Toggles now properly sync between action bar and menu
- Cleaner, more maintainable code
- Matches the proven working pattern
**Reference**: Compared with working version at commit d4ef91b
All three toggle functions (toggleCVLength, toggleIcons, toggleTheme) now
check if checkbox elements exist before setting their 'checked' property.
This prevents null pointer errors when:
- Menu checkboxes aren't rendered yet
- Functions are called programmatically before DOM is ready
- Tests call functions directly without full page render
Changed pattern from:
set checkbox's checked to true
To:
if checkbox exists
set checkbox's checked to true
end
Affected functions:
- toggleCVLength(): Lines 142-147, 154-159
- toggleIcons(): Lines 172-177, 183-188
- toggleTheme(): Lines 201-206, 212-217
The functions still work correctly when checkboxes exist, but now gracefully
handle missing elements instead of throwing null errors.
The CSS rules for showing/hiding icons target .cv-paper, not .cv-container.
Changed toggleIcons function to:
- Add/remove .show-icons class on .cv-paper (not .cv-container)
- This matches the CSS selectors in logo-toggle.css
Bug: Icons toggle wasn't working because class was being applied to wrong element
Fix: Apply .show-icons to .cv-paper to match CSS rules
Added 6 critical hyperscript functions that were lost during parse error fix:
- toggleCVLength(isLong) - Toggle between long/short CV views with localStorage
- toggleIcons(showIcons) - Toggle icon visibility with persistence
- toggleTheme(isClean) - Toggle between default/clean themes
- syncPdfHover(show) - Synchronized hover effects for PDF download buttons
- syncPrintHover(show) - Synchronized hover effects for print buttons
- highlightZoomControl(show) - Highlight zoom control on interaction
All functions use hyperscript 0.9.12 compatible syntax (no 'else' blocks).
Each toggle function syncs between action bar and menu checkboxes.
Verified:
- Zero parse errors in browser console
- All 9 hyperscript functions properly defined
- File grew from 134 to 250 lines (+87%)
- Compatible with existing HTML templates
Reverted static/hyperscript/functions._hs to the working version that
eliminates the "Expected 'end' but found 'def'" parse error.
The issue was caused by the expanded version with toggle functions.
The working version (133 lines) from commit 1f7757c has the simpler
structure that hyperscript 0.9.12 can parse correctly.
Verified with comprehensive browser testing:
- No parse errors in browser console
- Scroll behavior works correctly
- Back-to-top button shows/hides as expected
- All hyperscript functions load successfully
Switched from Flexbox to CSS Grid to enable true staggered animations.
Why Grid works better:
- Grid children maintain their grid cell during layout changes
- Buttons can transition individually within their cells
- grid-auto-flow change doesn't force simultaneous repositioning
Changes:
- Container: display: grid with grid-auto-flow
- Desktop: grid-auto-flow: row (vertical stack)
- Mobile: grid-auto-flow: column (horizontal row)
- Buttons: transition: all 0.5s with staggered delays (0s, 0.08s, 0.16s, 0.24s, 0.32s)
Result: Each button now cascades independently when resizing between
desktop and mobile layouts, creating the smooth wave effect.
The issue was that 'transition: all' on the container was forcing all
buttons to move together. Fixed by:
1. Container now transitions only: left, bottom, gap, transform
(NOT flex-direction or all properties)
2. Buttons transition only: transform, opacity, background-color, box-shadow, color
(NOT all properties)
3. Increased stagger delays for better visual effect:
- Button 1: 0s (instant)
- Button 2: 0.08s
- Button 3: 0.16s
- Button 4: 0.24s
- Button 5: 0.32s
4. Added flex-direction: 0s transition in mobile to instant switch direction
while buttons still cascade to new positions
Result: Each button now animates independently with cascading wave effect
when switching between desktop (vertical) and mobile (horizontal) layouts.