- Reorder cards: Short → Default ⭐ → Extended (was Short → Extended → Default)
- Add cleanup to prevent stuck blur effect on modal reopen
- Use MutationObserver to reset loading state when modal opens
- Add close event listener to clear loading-active class
Fixes:
1. Default CV now displays in center position as intended
2. Modal no longer shows grey/faded content due to stuck loading-active class
**Issue 1: URL corruption in "See this CV in..." links**
- Bug: replaceYearPlaceholder used fmt.Sprintf on ALL URLs
- URLs like "/?lang=es" were corrupted to "/?lang=es%!(EXTRA string=2025)"
- Fix: Changed to strings.ReplaceAll("{{YEAR}}", year)
- Result: Only replaces actual {{YEAR}} placeholders, leaves other URLs intact
**Issue 2: Download filename not respected**
- Bug: Shortcut URLs (cv-jamr-2025-en.pdf) redirected with HTTP 301
- Browsers used original URL filename instead of Content-Disposition header
- Fix: Generate PDF directly in DefaultCVShortcut handler
- Result: Returns PDF with correct filename in Content-Disposition header
Files changed:
- internal/models/cv.go: Fixed replaceYearPlaceholder function
- internal/handlers/cv.go: Changed redirect to direct PDF generation
Both fixes verified:
- "See this CV in Spanish" link: href="/?lang=es" ✓
- Download link: filename=cv-jamr-2025-en.pdf ✓
UX Improvement:
- Added visual feedback during PDF generation process
- Users now see immediate response when clicking download button
- Clear communication about what's happening during the wait
New Features:
- Loading overlay with animated spinner
- Format-specific estimated generation times (Short: ~3s, Default: ~4s, Long: ~8s)
- Blur effect on modal background during loading
- Bilingual support (English/Spanish)
- Automatic modal close after download completes
CSS Updates (static/css/04-interactive/_modals.css):
- Added .pdf-loading-overlay with glassmorphism effect
- Spinning animation for loader (1s linear infinite)
- Fade-in animation (300ms)
- Accessibility: respects prefers-reduced-motion
- Background blur when loading active
HTML Updates (templates/partials/modals/pdf-modal.html):
- Loading overlay structure with spinner
- Dynamic loading messages based on selected format
- Enhanced downloadPDF() function with timing logic
Before: Click → silence → download appears
After: Click → overlay + spinner + estimate → download appears
## Problem
- References section PDF link opened modal dialog
- Modal doesn't work in PDF or non-browser environments
- Link was not usable when CV exported as PDF
## Solution
- Changed from `onclick="openPdfModal()"` to direct shortcut URL
- Spanish CV: `/cv-jamr-2025-es.pdf`
- English CV: `/cv-jamr-2025-en.pdf`
- Language-aware: Uses {{$.Lang}} template variable
- Year-aware: Uses {{$.CurrentYear}} for auto-updates
## Benefits
- ✅ Works in any environment (PDF, browser, external links)
- ✅ Direct download without JavaScript/modal
- ✅ Shareable, permanent URLs
- ✅ Auto-updates yearly with no code changes
Technical details:
- File: templates/partials/sections/references.html:16
- Uses shortcut URLs created in previous feature
- Maintains target="_blank" and security attributes
## Shortcut URLs
- New routes: /cv-jamr-{year}-{lang}.pdf (e.g., /cv-jamr-2025-en.pdf)
- Year validation: Only current year accepted, returns 404 for past/future
- Auto-redirects (301) to: /export/pdf?lang={lang}&length=short&icons=show&version=with_skills
- Both languages supported: en and es
## PDF Modal Updates
- Replaced "Current View" option with "Default CV (Recommended)"
- Visual highlighting: purple gradient badge, star emoji ⭐, bold text
- Uses shortcut URL with dynamic year detection
- Clear recommendation for users (5 pages, short with skills)
## Technical Details
- Handler: DefaultCVShortcut() in internal/handlers/cv.go
- Pattern check in Home() handler for proper routing
- Helper function: window.openPdfModal() for references section
- Documentation: PDF-SHORTCUT-IMPLEMENTATION.md
Benefits:
- Memorable, shareable URLs (juan.andres.morenorub.io/cv-jamr-2025-en.pdf)
- Auto-updates yearly without code changes
- Clear user guidance for recommended CV format
- Created keyboard._hs as reference documentation (inline handler in body tag)
- Externalized 9 hamburger menu navigation links to scrollToSection()
- Added scrollToSection() as JavaScript function (CSP-safe, no eval needed)
- Restored original keyboard handler format in body tag (working correctly)
- Removed problematic navigation._hs (had syntax/CSP issues)
- Added Rule 4 to HYPERSCRIPT-RULES.md on event handler externalization
- Updated PROJECT-MEMORY.md with externalization guidelines
Key learnings:
- Complex event handlers that inspect event properties must stay inline
- JavaScript functions avoid CSP unsafe-eval restrictions
- Navigation successfully externalized: 9 links → 1 function (91% reduction)
Changed PDF export naming convention from 'detailed' to 'short' for better clarity and contrast with 'extended'. Updated:
- Documentation: All references from 'detailed' → 'short'
- JSON data files: Static PDF URLs now use cv-short-jamr-{{YEAR}}-{lang}.pdf
- Frontend modal: Removed 'short' → 'detailed' mapping (now stays as 'short')
- Static PDFs: Renamed cv-detailed-* to cv-short-* (deleted old files)
- Backend validation: Change
Problem: Inline icons embedded in responsibilities, courses, and
projects had explicit width='60' height='60' attributes that made
them too large (60px instead of ~16px).
Solution:
- Added CSS with !important to override inline width/height attributes
- Targeted inline icons in:
* Course responsibilities and descriptions
* Project descriptions and technologies
* Experience responsibilities (within divs)
- Preserved large icons (80px) for main company/course/project logos
Changes:
- static/css/03-components/_courses.css: Override to 1.2em
- static/css/03-components/_projects.css: Override to 1.2em
- static/css/03-components/_cv-section.css: Override to 1.2em
Test Results:
✅ 7 course inline icons: 16px × 16px
✅ Main company icons: 80px × 80px (preserved)
Change sidebar background from var(--sidebar-bg) to fixed #d1d4d2
so it remains the same light gray color in both light and dark themes.
The sidebar should provide a consistent visual anchor regardless
of the main content theme.
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
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 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()')
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 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.
Fixes:
1. ✅ Reversed button order - Info at bottom, Download at top
- Desktop (bottom→top): Info → Shortcuts → Zoom → Print → Download
- Mobile (left→right): Download → Print → Shortcuts → Info
2. ✅ Restored smooth morphing transitions
- Added transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1)
- Container smoothly transforms from vertical to horizontal
- Buttons smoothly reposition during responsive transition
3. ✅ Preserved all special hover behaviors
- Zoom button: Blue hover + highlights zoom control
- Print button: White bg + green icon on hover (synchronized)
- Download button: Red bg on hover (synchronized)
- All sync functions working: syncPdfHover, syncPrintHover, highlightZoomControl
The flexbox container now has the correct button order AND smooth animations!
Major architectural improvement for button management:
Desktop Layout (left side, vertical):
- Created .fixed-buttons-container positioned left: 2rem, bottom: 2rem
- Buttons stack vertically with flex-direction: column
- Order (bottom to top): Info → Shortcuts → Zoom → Print → Download
- 1rem (16px) gap between buttons
- All buttons 50×50px with consistent styling
- Easy to add/remove buttons - just add template to container
Mobile Layout (bottom center, horizontal):
- Container switches to flex-direction: row at @media (max-width: 900px)
- Centered with left: 50% and transform: translateX(-50%)
- 10px gap between buttons
- Zoom button hidden on mobile (display: none)
- 4 visible buttons: Info, Shortcuts, Print, Download
Benefits:
✅ Single source of truth - container manages all buttons
✅ Easy to add/remove buttons - no manual position calculations
✅ Responsive - automatic reflow from vertical to horizontal
✅ Maintainable - changes in one place affect all buttons
✅ Consistent spacing - gap property ensures even distribution
✅ Future-proof - adding button = adding template to container
Technical Implementation:
- Container uses position: fixed with flexbox
- Buttons use position: relative inside container (not fixed individually)
- All positioning managed by flexbox (gap, flex-direction, align-items)
- Hover states preserved with color-coded backgrounds per button
- Back-to-top button remains separate (not in container)
This replaces individual fixed positioning with a scalable flex layout.
This commit includes two major improvements:
1. Hyperscript Code Organization:
- Extracted all hyperscript blocks >3 lines into reusable functions
- Created 6 new functions in functions._hs:
* toggleCVLength(isLong) - CV length toggle sync
* toggleIcons(showIcons) - Icons toggle sync
* toggleTheme(isClean) - Theme toggle sync
* syncPdfHover(show) - PDF button synchronized hover
* syncPrintHover(show) - Print button synchronized hover
* highlightZoomControl(show) - Zoom control highlight effect
- Reduced inline hyperscript from 11+ lines to 1-2 lines per element
- Updated 8 template files to use function calls:
* hamburger-menu.html
* view-controls.html
* action-buttons.html
* download-button.html
* print-friendly-button.html
* zoom-toggle-button.html
2. Mobile Button Layout (max-width: 900px):
- Repositioned three fixed buttons (PDF, Print, Info) horizontally at bottom center
- Print button perfectly centered in viewport
- Download button on left, Info button on right
- Hidden zoom and shortcuts buttons on mobile (available in hamburger menu)
- Removed conflicting old mobile styles that were hiding print button
- Smooth hover transitions maintained with proper transform calculations
Technical details:
- Used CSS transform with calc() for precise horizontal positioning
- Maintained hover effects with combined translateX/translateY transforms
- Ensured accessibility with proper ARIA labels and spacing
- All functions check element existence before manipulation
- LocalStorage sync maintained across desktop/mobile toggles
- Removed .zoom-active CSS class and JavaScript logic
- Zoom button stays same gray color whether zoom is on or off
- Fixed Show Zoom menu button visibility (changed inline style to zoom-hidden class)
- Menu item now correctly appears when zoom is hidden
Print Button Improvements:
- Hover shows white background with green icon (was green bg + white icon)
- Applied to all print buttons: fixed, action bar, and menu
- Default state remains dark gray with white icon
Zoom Control Improvements:
- Text now 100% white (was 70-95% opacity) for better readability
- Increased font sizes: labels 0.95rem (was 0.8rem), current value 1.05rem (was 0.9rem)
- Hover zoom toggle button highlights zoom control with blue glow shadow
- Active zoom button uses semi-transparent blue rgba(52, 152, 219, 0.5) instead of solid
Z-Index Fix:
- Hamburger menu now appears above fixed buttons (z-index: 1000 vs 999)
- PDF buttons (fixed, action bar, menu) now sync hover states across all instances
- Print-friendly buttons (fixed, action bar, menu) sync green hover states
- White PDF icon with red hover background (#cd6060)
- Green print button hover (#27ae60)
- Implemented using hyperscript with .pdf-hover-sync and .print-hover-sync classes
- Creates cool visual feedback showing all related buttons simultaneously
PROBLEM:
- htmx:swapError with "Cannot read properties of null (reading 'insertBefore')" on double-click
- Toggle animations were "digital" (instant snap) instead of "analogical" (smooth slide)
- Conflict between server templates with hx-swap-oob and client-side hyperscript
ROOT CAUSE:
- Server templates returned HTML with hx-swap="outerHTML" + hx-swap-oob="true"
- This destroyed and recreated DOM elements during swap
- Second click tried to insert into null parent (element was destroyed)
- CSS transitions broke because element was destroyed mid-animation
SOLUTION:
- Remove all HTML from toggle templates (length-toggle.html, logo-toggle.html, theme-toggle.html)
- Templates now return empty comment: "<!-- Template not used - toggles use hx-swap="none" with inline hyperscript -->"
- Toggles use hx-swap="none" to prevent any DOM replacement
- All visual updates handled client-side via inline hyperscript
- Server only saves cookies in background (no HTML returned)
BENEFITS:
- ✅ No more null reference errors (no DOM destruction)
- ✅ Smooth CSS transitions work perfectly (element preserved)
- ✅ Desktop/mobile toggles sync via direct ID manipulation
- ✅ Zero HTMX swap conflicts
- ✅ Clean separation: client handles visuals, server persists state
DOCUMENTATION:
- Updated MODERN-WEB-TECHNIQUES.md with Phase 8
- Documented the complete debug journey and solution
- Added architecture pattern for client-first toggles
- Moved menu hover logic from JavaScript to CSS selectors, reducing JS to minimal bridge code
- Replaced JavaScript-based toast timing with pure CSS animation lifecycle (slide in → stay → fade out)
- Removed unnecessary event handlers and legacy compatibility code for cleaner implementation