```
**Update logout button (line 156):**
Replace:
```templ
```
With:
```templ
```
**Complete header.templ changes summary:**
- ✅ Wrapped nav in `x-data="{ themeDropdownOpen: false, userMenuOpen: false }"`
- ✅ Replaced all `@click="toggleXxx()"` with state toggles
- ✅ Replaced `id="xxx"` + `class="hidden"` with `x-show="xxxOpen"`
- ✅ Added `@click.outside` to both dropdowns
- ✅ Added `x-transition` for smooth animations
- ✅ Added namespace calls: `header.changeThemeTo()`, `header.logout()`
- ✅ Close dropdowns after action: `themeDropdownOpen = false`
### Step 1.2: Update header.ts
**Location**: `web/src/header.ts`
**Lines to delete**: 7-31, 64-88
**Delete manual DOM manipulation functions:**
```typescript
// DELETE lines 7-31:
const toggleThemeDropdown = (): void => {
const dropdown = document.getElementById("theme-dropdown");
if (dropdown) {
dropdown.classList.toggle("hidden");
const userMenu = document.getElementById("user-menu");
if (userMenu && !dropdown.classList.contains("hidden")) {
userMenu.classList.add("hidden");
}
}
};
const toggleUserMenu = (): void => {
const menu = document.getElementById("user-menu");
if (menu) {
menu.classList.toggle("hidden");
const themeDropdown = document.getElementById("theme-dropdown");
if (themeDropdown && !menu.classList.contains("hidden")) {
themeDropdown.classList.add("hidden");
}
}
};
```
**Simplify changeThemeTo function (lines 33-55):**
**Before:**
```typescript
const changeThemeTo = (theme: string): void => {
applyTheme(theme);
const token = localStorage.getItem("token");
if (token) {
fetch("/api/auth/theme", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ theme }),
}).catch((err) => console.log("Theme save failed", err));
}
// Close dropdown ← Manual DOM manipulation!
const dropdown = document.getElementById("theme-dropdown");
if (dropdown) {
dropdown.classList.add("hidden");
}
};
```
**After:**
```typescript
const changeThemeTo = (theme: string): void => {
// Apply theme
applyTheme(theme);
// Save to server if logged in
const token = localStorage.getItem("token");
if (token) {
fetch("/api/auth/theme", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ theme }),
}).catch((err) => console.log("Theme save failed", err));
}
// No dropdown manipulation - Alpine handles it via template state!
};
```
**Delete click-outside event listener (lines 64-88):**
```typescript
// DELETE entire event listener - Alpine's @click.outside handles this:
document.addEventListener("click", (e) => {
const target = e.target as HTMLElement;
const themeDropdown = document.getElementById("theme-dropdown");
const userMenu = document.getElementById("user-menu");
const themeButton = target?.closest(
'button[onclick="toggleThemeDropdown()"]',
);
const userButton = target?.closest('button[onclick="toggleUserMenu()"]');
if (
!themeButton &&
themeDropdown &&
!themeDropdown.classList.contains("hidden")
) {
if (!themeDropdown.contains(target)) {
themeDropdown.classList.add("hidden");
}
}
if (!userButton && userMenu && !userMenu.classList.contains("hidden")) {
if (!userMenu.contains(target)) {
userMenu.classList.add("hidden");
}
}
});
```
**Final header.ts (after cleanup):**
```typescript
// Header functionality
import { Alpine } from "./alpine";
import { applyTheme } from "./theme";
import { updateThemeIndicators } from "./themeDropdown";
const changeThemeTo = (theme: string): void => {
// Apply the theme using the consolidated function from theme.ts
applyTheme(theme);
// Save to server if logged in
const token = localStorage.getItem("token");
if (token) {
fetch("/api/auth/theme", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ theme }),
}).catch((err) => console.log("Theme save failed", err));
}
// Alpine closes dropdown automatically via template state
};
const logout = (): void => {
localStorage.removeItem("token");
localStorage.removeItem("user");
window.location.href = "/";
};
export { changeThemeTo, logout };
Alpine.global("header", {
logout,
changeThemeTo: (theme: string) => {
changeThemeTo(theme);
updateThemeIndicators();
},
});
```
**Result**: header.ts reduced from **100 lines to 40 lines** (60% reduction)
### Step 1.3: Create woodPaneling.ts Namespace
**Location**: `web/src/woodPaneling.ts`
**Current**: Functions exist but need Alpine registration for namespace calls
**Add to bottom of woodPaneling.ts:**
```typescript
import { Alpine } from "./alpine";
// ... existing functions ...
Alpine.global("woodPaneling", {
change: changeWoodPaneling,
});
```
### Step 1.4: Verify header.templ Migration
**Build verification:**
```bash
npm run build:ts
templ generate
```
**Expected**: No errors, templates compile successfully
**Manual testing:**
1. Start application: `go run .`
2. Navigate to any page with header (all pages)
3. Test theme dropdown:
- [ ] Click theme button → dropdown opens with smooth transition
- [ ] Click outside → dropdown closes
- [ ] Click theme option → theme changes, dropdown closes
- [ ] User menu closes if open
4. Test user menu:
- [ ] Click user button → menu opens with smooth transition
- [ ] Click outside → menu closes
- [ ] Click logout → logout, menu closes
- [ ] Theme dropdown closes if open
5. Test wood paneling:
- [ ] Click wood paneling option → changes, dropdown closes
**Debug with Alpine DevTools (optional):**
```bash
# Install Alpine DevTools browser extension
# Open DevTools → Alpine tab
# Inspect reactive state: themeDropdownOpen, userMenuOpen
```
**Common issues:**
- ❌ Dropdown doesn't open: Check if `x-data` is on parent container
- ❌ Dropdown doesn't close: Check if `@click.outside` is on dropdown div
- ❌ No smooth transition: Check if `x-transition` directives are present
- ❌ Flash of unstyled content: Verify `style="display: none;"` is on x-show elements
---
## Phase 2: Modal Templates
All modal templates follow the same pattern. Apply consistently.
### 2.1: collection_modal.templ
**Location**: `templates/collection_modal.templ`
**Time**: 1 hour
**Complexity**: Medium (modal with color/icon pickers)
**Template Changes:**
**Current (line 4):**
```templ
```
**No change needed** - `x-data="collections"` namespace is correct, but we need local state.
**Add local state to wrapper:**
Replace line 4:
```templ
```
**Better approach - use Alpine.data():**
**In web/src/collections.ts, add:**
```typescript
Alpine.data("collections", () => ({
colorModalOpen: false,
iconModalOpen: false,
openColorModal() {
this.colorModalOpen = true;
},
closeColorModal() {
this.colorModalOpen = false;
},
// ... existing methods ...
}));
```
**Actually, for modals, the simplest approach:**
Since this is a modal that's loaded via HTMX, we don't need x-data state. The modal itself is shown/hidden by HTMX.
**Skip this template** - it's already using Alpine namespace correctly via HTMX loading.
### 2.2: collections.templ
**Location**: `templates/collections.templ`
**Time**: 1-2 hours
**Complexity**: Medium (add books modal)
**Find "add-books-modal" section (lines 227-260):**
**Current:**
```templ
Add Books to Collection
```
**Replace with:**
```templ
Add Books to Collection
```
**Note**: The modal needs to be wrapped in x-data container that includes the button. Since the button and modal are far apart, we need to restructure.
**Better approach - use Alpine.store():**
**In web/src/collections.ts, add:**
```typescript
// Create a global store for modal state
Alpine.store('modals', {
addBooks: false,
showAddBooks() {
this.addBooks = true;
},
hideAddBooks() {
this.addBooks = false;
}
});
```
**In template, use:**
```templ
```
**Remove from TypeScript:**
In `web/src/collections.ts`, delete:
```typescript
function showAddBooksModal() { /* ... */ }
function hideAddBooksModal() { /* ... */ }
```
**Keep only business logic:**
```typescript
async function addSelectedBooks() { /* API call */ }
async function searchBooksForCollections() { /* API call */ }
```
### 2.3: conflicts.templ
**Location**: `templates/conflicts.templ`
**Time**: 1 hour
**Complexity**: Medium (resolution modal)
**Use Alpine.store pattern (same as collections):**
**In web/src/conflicts.ts, add:**
```typescript
Alpine.store('modals', {
resolveConflict: false,
showResolveConflict() {
this.resolveConflict = true;
},
hideResolveConflict() {
this.resolveConflict = false;
}
});
```
**Template changes:**
**Current (line 123):**
```templ
```
**Replace with:**
```templ
```
**Update button (line 110):**
Replace:
```templ