- 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)
22 KiB
CV Project - Developer Memory
⚠️ CRITICAL: Read This Before Any Changes
This document contains non-negotiable rules and hard-learned lessons from this project's development. Violating these causes bugs, breaks features, and wastes time.
🔴 ABSOLUTE RULES - NEVER VIOLATE
1. Icon Naming Convention (CRITICAL)
ALWAYS use "icons" - NEVER "logos"
// ✅ CORRECT
.show-icons
localStorage.getItem('cv-icons')
const showIcons = ...
// ❌ WRONG - WILL BREAK ICON TOGGLE
.show-logos
localStorage.getItem('cv-logos')
const showLogos = ...
Why: Icon toggle had critical bug (commit 3f77fed) because JavaScript used .show-logos but CSS checked for .show-icons. This caused toggles to only work after page refresh.
Test that enforces this: tests/mjs/1-toggles.test.mjs
Memory file: ~/.claude/cv-icons-migration.md
2. Hyperscript Parser Limit (REMOVED IN LATEST VERSION ✅)
✅ CONFIRMED: NO 3 def statement limit with latest hyperscript version
Test Results (2025-11-17): Test 9 (tests/mjs/9-hyperscript-def-limit.test.mjs) confirmed:
- ✅ 1 def statement works
- ✅ 2 def statements work
- ✅ 3 def statements work
- ✅ 4 def statements work (beyond historical limit)
- ✅ 5 def statements work (well beyond limit)
Historical Context:
- Hyperscript 0.9.12 had a hard 3 def limit
- Hyperscript 0.9.14+ removed this limitation
- Functions were moved to JavaScript as workaround
- NOW MIGRATED BACK to hyperscript with JavaScript wrappers (2025-11-17)
Current Architecture (2025-11-17):
- Core logic in hyperscript (
static/hyperscript/*.hs) - JavaScript wrappers for
callcommand compatibility (static/js/cv-functions.js) - Pattern:
window.fn()→_hyperscript.evaluate('hyperscriptFn()')
Current Best Practice: Organize hyperscript functions by category in separate files
static/hyperscript/
├── toggles._hs # Toggle functions (CV length, icons, theme)
├── hover-sync._hs # Hover synchronization functions
├── navigation._hs # Navigation functions (scroll-to-section) [2025-11-20]
├── keyboard._hs # Keyboard handler reference (inline in body tag)
└── utils._hs # Utility functions (print, scroll, etc.)
Migration in progress: Moving functions from cv-functions.js back to hyperscript
Test that verifies no limit: tests/mjs/9-hyperscript-def-limit.test.mjs
Reference: doc/4-HYPERSCRIPT-RULES.md
2.1. Hyperscript Event Handler Externalization (2025-11-20)
Rule: Complex event handlers that inspect event properties MUST stay inline
✅ CAN Be Externalized:
- Simple navigation handlers (scrollToSection)
- Toggle handlers (toggleCVLength, toggleIcons)
- Hover sync handlers (syncPdfHover, syncPrintHover)
❌ MUST Stay Inline:
- Keyboard handlers that inspect
event.key,event.target, modifier keys - Event handlers with complex conditional logic based on event properties
Why: The event variable in _="" attributes is a hyperscript runtime variable. External def functions don't have direct access to this event context from HTML attributes.
Optimization for Inline: Use then chains to make compact:
-- Compact inline handler with then chains
if event.key is '?' and not event.ctrlKey and not isInputField
halt the event then set modal to #shortcuts-modal then if modal then call modal.showModal() end
end
Example - Navigation (Externalized):
<!-- Clean HTML - 9 navigation links use single function -->
<a href="#education" _="on click call scrollToSection(event, 'education')">
-- External function in navigation._hs
def scrollToSection(event, sectionId)
call event.preventDefault()
set element to document.getElementById(sectionId)
if element then call element.scrollIntoView({behavior: 'smooth'}) end
end
Example - Keyboard Handler (Inline):
<!-- Must stay inline - inspects event.key, event.target, modifiers -->
<body _="on keydown
set tagName to event.target.tagName
set isInputField to (tagName is 'INPUT' or tagName is 'TEXTAREA')
if event.key is 'l' and not event.ctrlKey and not isInputField
halt the event then -- handler logic
end
end">
Files:
static/hyperscript/navigation._hs- External navigation functiontemplates/partials/navigation/hamburger-menu.html- 9 clean navigation linkstemplates/index.html- Optimized inline keyboard handler (body tag)
Test: tests/mjs/2-keyboard-shortcuts.test.mjs (keyboard shortcuts)
Reference: doc/4-HYPERSCRIPT-RULES.md Section "Rule 4: Event Handler Externalization Guidelines"
3. Toggle Synchronization (WORKING) + Hover Sync (FIXED)
Toggle checkboxes WORK perfectly ✅
Every toggle checkbox exists in TWO places and stays synchronized:
- Action bar (desktop) -
#lengthToggle,#iconToggle,#themeToggle - Hamburger menu (mobile) -
#lengthToggleMenu,#iconToggleMenu,#themeToggleMenu
Toggle behavior (WORKING):
- ✅ Clicking either toggle updates BOTH
- ✅ Changes are real-time (no page refresh)
- ✅ localStorage persists the state
- ✅ Page load reads from localStorage and applies to both
// ✅ CORRECT - Update both toggles
function toggleIcons(showIcons) {
const paper = document.querySelector('.cv-paper');
const otherToggle = document.querySelector('#iconToggle') ||
document.querySelector('#iconToggleMenu');
if (showIcons) {
paper?.classList.add('show-icons');
localStorage.setItem('cv-icons', 'true');
if (otherToggle) otherToggle.checked = true;
} else {
paper?.classList.remove('show-icons');
localStorage.setItem('cv-icons', 'false');
if (otherToggle) otherToggle.checked = false;
}
}
✅ HOVER SYNC - PDF/Print buttons NOW WORKING
Action buttons that sync hover states:
- Action bar (desktop):
#action-bar-pdf-btn- Download PDF button.action-bar-print-btn- Print Friendly button
- Hamburger menu (mobile):
.menu-pdf-btn- PDF button (templates/partials/navigation/hamburger-menu.html:178).menu-print-btn- Print button (templates/partials/navigation/hamburger-menu.html:186)
Working hover sync functions:
syncPdfHover(isHovering)- Syncs PDF button hover between locations ✅syncPrintHover(isHovering)- Syncs Print button hover between locations ✅
Implementation:
- Both buttons in each location have mouseenter/mouseleave handlers
- Sync functions select all matching buttons (
.pdf-btn, .menu-pdf-btn) - CSS classes
.pdf-hover-syncand.print-hover-syncapplied to all instances - Visual feedback matches across action bar and menu
Files involved:
templates/partials/navigation/action-buttons.html(lines 9-10, 18-19)templates/partials/navigation/hamburger-menu.html(lines 178-184, 186-189)static/js/cv-functions.js(lines 71-99)static/css/main.css(lines 2690-2712)
Test coverage: tests/mjs/8-hover-sync.test.mjs ✅
Test that enforces toggle sync: tests/mjs/1-toggles.test.mjs ✅
4. Real-Time Rendering + Persistence (CRITICAL)
ALL UI changes MUST happen in BOTH DOM and localStorage
BOTH are required - not mutually exclusive:
// ✅ CORRECT - DOM update AND localStorage
paper.classList.add('show-icons'); // Visual change (instant feedback)
localStorage.setItem('cv-icons', 'true'); // Persistence (survives reload)
// ❌ WRONG - Only DOM (lost on page reload)
paper.classList.add('show-icons');
// ❌ WRONG - Only storage (no visual feedback)
localStorage.setItem('cv-icons', 'true');
Why both are needed:
- DOM changes = instant visual feedback (users see it immediately)
- localStorage = state persists across page reloads (users don't lose their preferences)
- Users need BOTH: instant feedback + remembered preferences
Test that enforces this: tests/mjs/1-toggles.test.mjs (verifies both DOM and localStorage)
5. Test Suite as Single Source of Truth
If a test passes, the feature MUST work. If a feature works, the test MUST pass.
tests/mjs/
├── 0-zoom.test.mjs # Zoom functionality
├── 1-toggles.test.mjs # ALL toggles + sync + persistence
├── 2-keyboard-shortcuts.test.mjs # L, I, V, ? keys
├── 3-hyperscript.test.mjs # Parse errors + integrity
├── 4-htmx.test.mjs # HTMX integration
├── 5-language.test.mjs # EN/ES switching
├── 6-modals.test.mjs # Modal functionality
├── 7-mobile-responsive.test.mjs # Mobile viewports
├── 8-hover-sync.test.mjs # PDF/Print button hover sync
└── 9-hyperscript-def-limit.test.mjs # Def statement limit verification
Non-negotiable:
- ❌ NO code changes without test validation
- ❌ NO bug fixes without regression test
- ❌ NO features without test coverage
- ❌ NO deployment if tests fail
Run before every commit:
bun tests/run-all.mjs
Reference: tests/README.md, tests/TEST-SUMMARY.md
📋 Architecture Decisions
Tech Stack
Backend:
- ✅ Go 1.21+ (Backend server)
- ✅ Standard library
net/http(HTTP server) - ✅ Go templates (Server-side rendering)
Frontend:
- ✅ HTMX (Hypermedia-driven interactions)
- ✅ Hyperscript (Latest version - event handling)
- ✅ Vanilla JavaScript (cv-functions.js - toggles, keyboard shortcuts)
- ✅ Iconify (Icon system)
Testing:
- ✅ Playwright (E2E browser automation)
- ✅ Bun (Test runner ONLY - NOT the runtime!)
Why Go:
- Fast compilation
- Single binary deployment
- Built-in templating
- Excellent standard library
- Strong typing
- Cross-platform
Why HTMX + Hyperscript:
- Server-driven UI (hypermedia pattern)
- Minimal JavaScript
- Progressive enhancement
- Real-time updates capabilities
File Organization
cv/
├── main.go # Server entry point (v1.1.0)
├── go.mod, go.sum # Go dependencies
├── internal/
│ ├── config/ # Configuration
│ ├── handlers/ # HTTP handlers
│ ├── middleware/ # HTTP middleware
│ ├── models/ # Data models
│ ├── routes/ # Route definitions
│ └── templates/ # Template utilities
├── static/
│ ├── js/
│ │ └── cv-functions.js # Global functions (toggles, keyboard, hover sync)
│ ├── css/ # Stylesheets
│ ├── hyperscript/
│ │ └── functions._hs # Hyperscript functions (if any)
│ ├── images/ # Static images
│ └── pdf/ # PDF files
├── templates/
│ ├── index.html # Main page template
│ ├── cv-content.html # CV content
│ ├── language-switch.html # Language switcher
│ └── partials/
│ ├── navigation/
│ │ ├── action-bar.html # Desktop action bar
│ │ ├── action-buttons.html # PDF + Print buttons
│ │ └── hamburger-menu.html # Mobile menu (ALL controls + buttons)
│ ├── widgets/ # Reusable UI components
│ └── modals/ # Modal dialogs
└── tests/
└── mjs/ # Systematic test suite (Playwright + Bun runner)
Critical files:
main.go- Server entry pointstatic/js/cv-functions.js- Toggle, keyboard, and hover sync functionstemplates/partials/navigation/action-buttons.html- PDF + Print buttons (action bar)templates/partials/navigation/hamburger-menu.html- Complete mobile menu with toggles + buttonstemplates/index.html- Main template structure
🐛 Historical Bugs (Learn From These)
Bug 1: Icon Toggle Required Refresh
Commit: 3f77fed
Date: 2025-11-17
Symptom: Icon toggle only worked after page refresh
Root cause: Class name mismatch
- JavaScript:
classList.add('show-logos') - CSS:
.cv-paper:not(.show-icons)Fix: Changed all references from "logos" to "icons" Test added: Screenshot verification in1-toggles.test.mjsMemory:~/.claude/cv-icons-migration.md
Bug 2: Hyperscript Parser Crash
Date: 2025-11-16
Symptom: Silent failures, toggles stopped working
Root cause: Exceeded 3 def statement limit
Fix: Moved toggle logic to JavaScript (cv-functions.js)
Test added: Def statement counter in 3-hyperscript.test.mjs
Reference: HYPERSCRIPT-RULES.md
Bug 3: Toggle Desynchronization
Date: 2025-11-15
Symptom: Action bar and menu toggles out of sync
Root cause: Only updating one toggle, not both
Fix: Every toggle function now updates both locations
Test added: Sync verification in 1-toggles.test.mjs
🎯 Development Workflow
Before Making Changes
- Read relevant test: Understand what's being tested
- Run the test: See current behavior
- Make changes: Code + test updates together
- Run test again: Verify it passes
- Run ALL tests:
bun tests/run-all.mjs - Manual verification: Browser stays open - check it yourself
- Commit: With clear description
When Adding a Feature
- Write test FIRST (test what you want to build)
- Implement feature
- Test passes (feature works)
- Update documentation (TEST-SUMMARY.md)
- Commit both (code + test together)
When Fixing a Bug
- Write regression test (reproduces the bug)
- Test FAILS (proves bug exists)
- Fix the bug
- Test PASSES (proves fix works)
- Commit both (fix + test together)
📝 Key Patterns
Toggle Pattern (Standard)
function toggleX(enabled) {
const target = document.querySelector('.target');
const otherToggle = document.querySelector('#xToggle') ||
document.querySelector('#xToggleMenu');
if (enabled) {
target?.classList.add('state-class');
localStorage.setItem('cv-x', 'true');
if (otherToggle) otherToggle.checked = true;
} else {
target?.classList.remove('state-class');
localStorage.setItem('cv-x', 'false');
if (otherToggle) otherToggle.checked = false;
}
}
Must have:
- ✅ Real-time DOM update
- ✅ localStorage persistence
- ✅ Sync both toggle locations
- ✅ Safe navigation (
?.)
Hyperscript Pattern (LIMITED USE)
<!-- ✅ GOOD - Simple event delegation -->
<button _="on click call myJavaScriptFunction(my.checked)">
<!-- ⚠️ AVOID - Complex logic (use JS instead) -->
<button _="on click
if condition then ...
else ... end">
<!-- ❌ FORBIDDEN - def statements (unless absolutely necessary) -->
<script type="_hyperscript">
def myFunction() <!-- COUNTS TOWARD 3 LIMIT -->
...
end
</script>
Keyboard Shortcuts Pattern
document.addEventListener('keydown', (e) => {
// ✅ ALWAYS check for input fields
if (e.target.matches('input, textarea, select')) return;
// ✅ Use lowercase key
const key = e.key.toLowerCase();
switch(key) {
case 'l': toggleCVLength(!body.classList.contains('cv-long')); break;
case 'i': toggleIcons(!paper.classList.contains('show-icons')); break;
case 'v': toggleTheme(!body.classList.contains('theme-clean')); break;
case '?': document.querySelector('#shortcuts-modal')?.showModal(); break;
}
});
Test: tests/mjs/2-keyboard-shortcuts.test.mjs
🔧 Common Operations
Adding a New Toggle
- Add to HTML: Action bar + menu
- Add localStorage key: Choose naming convention
- Add toggle function: Follow standard pattern
- Add keyboard shortcut: Optional but recommended
- Add to test: Update
1-toggles.test.mjs - Add to keyboard test: If you added shortcut
- Run ALL tests
Changing Class Names
- Search globally: Find ALL references
- Update JavaScript: cv-functions.js
- Update CSS: All stylesheets
- Update HTML: All templates
- Update tests: Search for old name
- Create memory file:
~/.claude/name-migration.md - Test thoroughly
Debugging Toggle Issues
Checklist:
- Check class name matches between JS and CSS
- Verify both toggles are updated (action bar + menu)
- Check localStorage key is correct
- Verify real-time DOM update happens
- Run
1-toggles.test.mjs- does it catch the bug? - If test doesn't catch it, FIX THE TEST FIRST
📚 Key Documents
Must Read Before Changes
tests/README.md- Test suite accountabilitydoc/HYPERSCRIPT-RULES.md- Parser limits and workarounds~/.claude/cv-icons-migration.md- Icon naming convention
Reference Documentation
tests/TEST-SUMMARY.md- Complete test documentationtests/mjs/README.md- Test structure and patternsdoc/MODERN-WEB-TECHNIQUES.md- Architecture decisions
Historical Record
tests/archive/README.md- Legacy tests (reference only)- Git history - Search for bug fix commits
🎓 Lessons Learned
1. Name Things Correctly From the Start
Lesson: Renaming "logos" to "icons" required:
- Updating 4 files
- Creating memory document
- Adding test verification
- Could have been avoided with better initial naming
Rule: Think about naming conventions BEFORE first implementation
2. Tests Are Not Optional
Lesson: Icon toggle bug existed for days before we added screenshot verification to the test. The test was passing but the feature was broken.
Rule: Tests must verify ACTUAL behavior, not just code execution
3. Framework Limits Are Real
Lesson: Hyperscript's 3 def limit seems arbitrary but it's a hard constraint. We learned this after hitting silent failures.
Rule: Read framework documentation carefully, especially limits/constraints
4. Synchronization Is Hard
Lesson: Having toggles in two or more places (action bar + menu) means every change must update all locations. Forgetting this causes desync bugs.
Rule: If something appears in multiple places, use a single function to update ALL locations
5. Real-Time Updates Are Expected
Lesson: Users don't understand localStorage vs DOM updates. If they click a button, they expect INSTANT visual feedback.
Rule: Every toggle must update both localStorage AND DOM immediately
🚀 Future Developers
Before you change ANYTHING:
- Read this document
- Read
tests/README.md - Run
bun tests/run-all.mjs - Understand which test covers what you're changing
- Make your changes + update tests together
- Run ALL tests again
- Manual verification in browser
- Update this document if you learn something new
When you hit a bug:
- Which test should have caught this?
- Why didn't it?
- Fix the test FIRST
- Then fix the bug, using the debug agent and the htmx expert
- Document the lesson in this file
Remember:
- Tests are the specification
- If tests pass but feature fails → tests are wrong
- If feature works but tests fail → feature is wrong
- When in doubt, trust the tests MORE than the code
Last Updated: 2025-11-17
Project Status: Production - Migrating to hyperscript architecture
Test Coverage: 10 systematic tests, 100% core features + def limit verification
Critical Memory Files: This file + ~/.claude/cv-icons-migration.md
3. Hyperscript-JavaScript Interoperability (CRITICAL - 2025-11-17)
Rule: Hyperscript call in attributes requires global JavaScript scope
<!-- ❌ DOESN'T WORK - Hyperscript def not in window -->
<button _="on click call hyperscriptFunction()">Click</button>
<!-- ✅ WORKS - JavaScript wrapper exposes to window -->
window.functionName = () => _hyperscript.evaluate('hyperscriptFunction()');
Why this matters:
- Hyperscript docs say "global hyperscript functions can be called from JavaScript" ✅ TRUE
- BUT the reverse (
callin_=""attributes) requires functions inwindowobject - Hyperscript
deffunctions are NOT automatically exposed to window - Templates use
_="on mouseenter call syncPdfHover(true)"syntax
Solution - Wrapper Pattern:
// static/js/cv-functions.js
function syncPdfHover(show) {
if (typeof _hyperscript !== 'undefined') {
_hyperscript.evaluate('syncPdfHover(' + show + ')');
}
}
window.syncPdfHover = syncPdfHover;
Files:
- Implementation:
static/hyperscript/*.hs(toggles._hs, hover-sync._hs) - Wrappers:
static/js/cv-functions.js - Test:
tests/mjs/8-hover-sync.test.mjs
Bug History: Hover sync broke when JavaScript functions were deleted during hyperscript migration. Restored as thin wrappers (commit 491aa66).
4. Zoom Architecture (CRITICAL - 2025-11-17)
Rule: Only CV content inside #zoom-wrapper, NOT UI chrome
<!-- ✅ CORRECT Structure -->
<div id="zoom-wrapper">
<div class="cv-container">CV Content</div>
</div>
{{template "page-footer" .}} <!-- OUTSIDE zoom-wrapper -->
What gets zoomed (INSIDE #zoom-wrapper):
- ✅ CV paper (.cv-container)
- ✅ CV content (.cv-paper)
What does NOT get zoomed (OUTSIDE #zoom-wrapper):
- ✅ Footer
- ✅ Action bar
- ✅ Hamburger menu
- ✅ Fixed buttons (PDF, print, zoom toggle, etc.)
Zoom Range: 25% - 300% (updated from 175% on 2025-11-17)
localStorage Keys:
cv-zoom- Current zoom level (25-300)cv-zoom-visible- Whether zoom control is shown (true/false)cv-zoom-position- Draggable position of zoom control
Critical Bug Fixed (commit 52e97f1):
- Footer was INSIDE zoom-wrapper → got zoomed with content
- Moved footer OUTSIDE zoom-wrapper → stays normal size
- Test:
tests/mjs/11-zoom-ui-exclusion.test.mjs
Critical Bug Fixed (commit 35a836a):
- Zoom persistence broken - set wrong element's value
- zoom-control.html:10 was
set my value(div) instead ofset #zoom-slider's value - Test:
tests/mjs/10-zoom-persistence.test.mjs
5. Test Maintenance (REQUIRED - 2025-11-17)
Rule: Update tests/TEST-SUMMARY.md every time you add a test
When adding new test files:
- Create test file:
tests/mjs/{N}-{feature}.test.mjs - Update
tests/TEST-SUMMARY.mdwith test description - Update test count at bottom of TEST-SUMMARY.md
- Add to New Tests section with date
Current Test Count: 12 active (0-11), 60+ archived
Master test runner: tests/run-all.mjs (auto-discovers numbered tests)