Replace 34-line IIFE/MutationObserver with:
- Hyperscript: on toggle if me.open call resetContactForm()
- 11-line resetContactForm() function
Also dispatches 'show' event from openModal() for ninja-keys integration.
All 7 contact form tests pass.
Implement a command palette accessible via CMD+K/Ctrl+K using the ninja-keys
web component. Features include:
- New /api/cmd-k endpoint serving dynamic CV entries (experiences, projects, courses)
- Language-aware responses with 1-hour cache headers
- Scroll-to-section functionality for quick navigation
- Enhanced keyboard shortcuts modal with CMD+K documentation
- Comprehensive test coverage for API and UI interactions
Also includes cleanup of deprecated debug test files and various UI polish
improvements to contact form, themes, and action bar components.
First-time visitors now always see light theme (paper aesthetic)
regardless of their system dark mode preference.
Users can still switch to dark or auto mode, and their preference
is saved to localStorage for future visits.
This maintains the professional CV paper appearance as the default
experience while giving users full control over their preference.
- Add scrollToSection() to utils._hs (was missing after cv-functions.js removal)
- Move error toast close handler to inline hyperscript
- Remove initMenuCloseOnClick() - now integrated into scrollToSection()
- Remove initErrorToastClose() - now hyperscript inline handler
- Remove unused initScrollBehaviorJS() fallback (~70 lines dead code)
This fixes the navigation menu scroll functionality and eliminates
more JavaScript in favor of hyperscript.
- Move initZoomControlButtons() from main.js to hyperscript handlers
- zoom-toggle-button: on click call toggleZoomControl()
- zoom-close: on click call hideZoomControl()
- show-zoom-menu-btn: on click call showZoomControl()
- Move expandAllSections/collapseAllSections from JS to utils._hs
- Add zoom visibility functions to zoom._hs:
- showZoomControl(), hideZoomControl(), toggleZoomControl()
- Update hamburger menu links to use hyperscript calls
Eliminates ~75 more lines of JavaScript in favor of declarative
hyperscript, continuing the pattern of moving behavior to ._hs files.
Root cause: overflow-x: hidden on html/body elements breaks position: sticky
on descendant elements. This is a known CSS behavior.
Changes:
- _reset.css: Changed overflow-x from 'hidden' to 'clip' on html and body
- 'clip' prevents horizontal scrolling WITHOUT breaking sticky positioning
- index.html: Restored hyperscript scroll handlers (initScrollBehavior, handleScroll)
- main.js: Disabled JavaScript scroll fallback in favor of hyperscript
Behavior:
- Desktop: Action bar hides on scroll down, reappears on scroll up
- Mobile (≤900px): Action bar stays visible at all times (CSS override)
Tested: Both desktop and mobile scroll behaviors work correctly
The hyperscript-based scroll behavior was not working reliably across all browsers.
Replaced with a pure JavaScript implementation that:
Desktop (>900px):
- Hides action bar on scroll down (past 100px threshold)
- Shows action bar on scroll up
- Shows action bar at top of page
Mobile (≤900px):
- Always keeps action bar visible
- Actively removes header-hidden class on mobile
- Handles viewport resize for responsive testing
Changes:
- Added initScrollBehaviorJS() function to main.js
- Removed hyperscript scroll handlers from body tag in index.html
- Kept keyboard shortcut handlers in hyperscript (still working)
- Uses passive scroll listener for better performance
This fixes the bug where:
- Desktop: bar would hide but not show again on scroll up
- Mobile: bar was incorrectly hiding despite CSS override
Issues fixed:
1. Zoom button now uses purple color (rgba(155, 89, 182, 0.7)) instead of blue
2. Info button keeps blue color (rgba(52, 152, 219, 0.7))
3. Both buttons now show distinct colors in default state, not just on hover
4. Device detection now considers viewport width, not just user agent
5. Buttons no longer hide in responsive mode at desktop viewport sizes
Changes:
- Updated zoom-toggle-btn to use purple background color
- Updated info-button to use blue background color (explicit, not var)
- Modified device-detection.js to check viewport width (≤900px) in addition to UA
- Added resize listener to update device class dynamically
- Created test (67-button-colors-and-visibility-test.mjs) to verify fixes
Testing:
- Desktop (1278px): All buttons visible with distinct colors
- Mobile (375px): Zoom/shortcuts hidden, core buttons visible
- Device detection now viewport-aware (prevents hiding at desktop sizes)
1. Removed unused getPreferenceCookie and setPreferenceCookie functions
- These were flagged by golangci-lint as unused
- Cookie preferences now handled client-side via localStorage
- Removed unused net/http import
2. Fixed desktop sidebar accordion auto-opening
- Updated handleLandscapeAccordions() to open accordions in desktop view (≥769px)
- Sidebars now show content in desktop, landscape mobile, and portrait mobile
- Only keep accordions collapsed in portrait mobile for space saving
3. Created comprehensive multi-viewport test (66-comprehensive-all-viewports-test.mjs)
- Tests desktop (1278px), portrait mobile (375×667), landscape mobile (667×375)
- Validates sidebars, accordion state, content visibility, AND all buttons
- Checks button backdrop visibility in mobile views
- Every feature now has corresponding test coverage
Fixes golangci-lint errors:
- internal/handlers/cv_helpers.go:366: func getPreferenceCookie is unused
- internal/handlers/cv_helpers.go:375: func setPreferenceCookie is unused
- internal/handlers/cv_helpers.go:7: net/http imported and not used
Issue 1: Blur bar compatibility (Android doesn't always show at bottom)
✅ Solution: Wrap blur bar in @supports query for backdrop-filter
- Only shows on devices that support backdrop-filter (primarily iOS)
- Android devices without support won't see the bar
- Prevents layout issues on non-iOS devices
Issue 2: Keyboard shortcuts button on real mobile (no physical keyboard)
✅ Solution: Device detection + conditional hiding
- Added device-detection.js: Detects real mobile vs desktop browser
- Checks user agent (Android, iPhone, iPad, etc.) + touch support
- Adds 'is-mobile-device' or 'is-desktop' class to <html>
- CSS hides shortcuts button only on real mobile devices
- Desktop browser in mobile view: shortcuts button still visible (for testing)
Implementation Details:
1. Device Detection (static/js/device-detection.js):
- User agent detection: /Android|iPhone|iPad|etc./
- Touch support check: ontouchstart + maxTouchPoints
- Class added to <html>: is-mobile-device or is-desktop
2. Blur Bar (@supports query):
- Detects backdrop-filter support before applying
- iOS: Shows blur bar with backdrop-filter
- Android (most): No blur bar (no backdrop-filter support)
- Prevents empty/broken bar on incompatible devices
3. CSS Hiding Rules:
- .is-mobile-device .shortcuts-btn { display: none !important; }
- Also hides zoom-toggle-btn and zoom-control on real mobile
- Desktop mobile view: shortcuts button remains visible
Files Modified:
- static/js/device-detection.js: NEW - Device detection logic
- templates/index.html: Load device-detection.js early
- static/css/05-responsive/_breakpoints.css: @supports wrapper for blur bar
- static/css/04-interactive/_scroll-behavior.css: Hide shortcuts on real mobile
- tests/mjs/52-mobile-device-detection-test.mjs: Comprehensive device detection test
Test Results:
✅ iPhone (real mobile): is-mobile-device class, shortcuts hidden
✅ Desktop browser (mobile view): is-desktop class, shortcuts visible
✅ Blur bar: Only shows on devices with backdrop-filter support
Fixed two critical mobile view issues:
1. Extended CV Sidebar Accordion:
- Updated sidebar.html to use native <details> element (was div with onclick)
- Styled accordion header to match CV title badges dark theme (#303030)
- Applied consistent styling: dark gray background, light text, uppercase, no spacing
- Result: Sidebars now collapse/expand properly with native HTML functionality
2. PDF Download Modal Centering:
- Added JavaScript-based centering for mobile viewports (≤768px)
- Uses inline styles with !important flag to override browser defaults
- Updated download button to call openPdfModal() function
- Result: Modal is perfectly centered on mobile (0px offset)
Technical notes:
- Modal centering required setProperty() with 'important' flag
- Accordion matches cv-title-badges-header style exactly
- All tests passing: accordion toggle, modal centering
Files modified:
- templates/partials/cv/sidebar.html
- static/css/05-responsive/_breakpoints.css
- static/js/main.js
- templates/partials/widgets/download-button.html
Tests added:
- tests/mjs/43-mobile-accordion-and-modal-test.mjs
- tests/mjs/46-visual-accordion-style-test.mjs
**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 ✓
## 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)
The toggleTheme() function targets .cv-container but initPreferences()
was adding theme-clean to document.body, causing state mismatch.
First click would do nothing because toggle state was inverted.
- Change initPreferences() to target .cv-container
- Fix HTMX swap handler to check .cv-container instead of body
- Now first click works immediately in both directions
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
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 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.
- 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
- 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
- Replace CSS zoom with transform: scale() for proper viewport extension
- Add dynamic margin-bottom to position footer correctly
- Remove zoom: 1 reset from fixed buttons (no longer needed)
- Enables true zoom from 10% to 500% that extends beyond viewport
- Add green background (#27ae60) to info and back-to-top buttons when at page bottom
- Implement bottom detection (within 50px threshold) in scroll handler
- Add conditional green hover to zoom reset button (only when zoom ≠ 100%)
- Enhance UX with visual feedback for scroll position and zoom state
Switched from transform: scale() to CSS zoom property on zoom-wrapper.
CSS zoom changes actual layout space, not just visual rendering:
- At 50% zoom, wrapper takes 50% space (no reserved empty space)
- Footer naturally follows right after zoomed content
- At 200% zoom, content extends beyond viewport with scrolling
- Fixes the large gray gap between content and footer
- Created zoom-wrapper div around cv-container
- Zoom now applies to wrapper only, footer adjusts naturally below
- Footer no longer scaled, stays at normal size
- Fixes gap between content and footer at low zoom levels
- Reduced back-to-top button size (35px default, grows to 50px on hover)
- Cleaner separation of concerns for zoom functionality
- Moved "Zoom" button from action bar to hamburger menu under "Acciones Rápidas"
- Close button (X) now grey/subtle by default (opacity: 0.7)
- Close button turns red only on hover for clear indication
- Updated JavaScript to reference show-zoom-menu-btn instead of show-zoom-btn
- Added preventDefault to showZoomControl to prevent link navigation