+```
+
+**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
+