Files
cv-site/PROJECT-MEMORY.md
T
juanatsap 925a95c1b4 refactor: externalize navigation handlers and fix hyperscript syntax errors
- 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)
2025-11-20 09:46:06 +00:00

684 lines
22 KiB
Markdown

# 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"**
```javascript
// ✅ 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 `call` command 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:
```hyperscript
-- 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):**
```html
<!-- Clean HTML - 9 navigation links use single function -->
<a href="#education" _="on click call scrollToSection(event, 'education')">
```
```hyperscript
-- 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):**
```html
<!-- 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 function
- `templates/partials/navigation/hamburger-menu.html` - 9 clean navigation links
- `templates/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:
1. Action bar (desktop) - `#lengthToggle`, `#iconToggle`, `#themeToggle`
2. 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
```javascript
// ✅ 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:**
1. Action bar (desktop):
- `#action-bar-pdf-btn` - Download PDF button
- `.action-bar-print-btn` - Print Friendly button
2. 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-sync` and `.print-hover-sync` applied 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:**
```javascript
// ✅ 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:**
```bash
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 point
- `static/js/cv-functions.js` - Toggle, keyboard, and hover sync functions
- `templates/partials/navigation/action-buttons.html` - PDF + Print buttons (action bar)
- `templates/partials/navigation/hamburger-menu.html` - Complete mobile menu with toggles + buttons
- `templates/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 in `1-toggles.test.mjs`
**Memory:** `~/.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
1. **Read relevant test:** Understand what's being tested
2. **Run the test:** See current behavior
3. **Make changes:** Code + test updates together
4. **Run test again:** Verify it passes
5. **Run ALL tests:** `bun tests/run-all.mjs`
6. **Manual verification:** Browser stays open - check it yourself
7. **Commit:** With clear description
### When Adding a Feature
1. **Write test FIRST** (test what you want to build)
2. **Implement feature**
3. **Test passes** (feature works)
4. **Update documentation** (TEST-SUMMARY.md)
5. **Commit both** (code + test together)
### When Fixing a Bug
1. **Write regression test** (reproduces the bug)
2. **Test FAILS** (proves bug exists)
3. **Fix the bug**
4. **Test PASSES** (proves fix works)
5. **Commit both** (fix + test together)
---
## 📝 Key Patterns
### Toggle Pattern (Standard)
```javascript
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)
```html
<!-- ✅ 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
```javascript
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
1. **Add to HTML:** Action bar + menu
2. **Add localStorage key:** Choose naming convention
3. **Add toggle function:** Follow standard pattern
4. **Add keyboard shortcut:** Optional but recommended
5. **Add to test:** Update `1-toggles.test.mjs`
6. **Add to keyboard test:** If you added shortcut
7. **Run ALL tests**
### Changing Class Names
1. **Search globally:** Find ALL references
2. **Update JavaScript:** cv-functions.js
3. **Update CSS:** All stylesheets
4. **Update HTML:** All templates
5. **Update tests:** Search for old name
6. **Create memory file:** `~/.claude/name-migration.md`
7. **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 accountability
- `doc/HYPERSCRIPT-RULES.md` - Parser limits and workarounds
- `~/.claude/cv-icons-migration.md` - Icon naming convention
### Reference Documentation
- `tests/TEST-SUMMARY.md` - Complete test documentation
- `tests/mjs/README.md` - Test structure and patterns
- `doc/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:**
1. Read this document
2. Read `tests/README.md`
3. Run `bun tests/run-all.mjs`
4. Understand which test covers what you're changing
5. Make your changes + update tests together
6. Run ALL tests again
7. Manual verification in browser
8. Update this document if you learn something new
**When you hit a bug:**
1. Which test should have caught this?
2. Why didn't it?
3. Fix the test FIRST
4. Then fix the bug, using the debug agent and the htmx expert
5. 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**
```html
<!-- ❌ 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 (`call` in `_=""` attributes) requires functions in `window` object
- Hyperscript `def` functions are NOT automatically exposed to window
- Templates use `_="on mouseenter call syncPdfHover(true)"` syntax
**Solution - Wrapper Pattern:**
```javascript
// 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**
```html
<!-- ✅ 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 of `set #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:
1. Create test file: `tests/mjs/{N}-{feature}.test.mjs`
2. Update `tests/TEST-SUMMARY.md` with test description
3. Update test count at bottom of TEST-SUMMARY.md
4. 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)