# Alpine.js Integration Completion Guide

## Executive Summary

This guide completes the migration from **hybrid onclick/@click with manual DOM manipulation** to **full reactive Alpine.js** with state-driven UI.

**Current State**: Hybrid approach with 121 manual DOM manipulations
**Target State**: Full reactive Alpine.js with zero manual DOM manipulation
**Estimated Time**: 10-12 hours
**Impact**: Cleaner code, better maintainability, smoother UX

---

## Table of Contents

1. [Current State Analysis](#current-state-analysis)
2. [Migration Strategy](#migration-strategy)
3. [Phase 1: Header Template (Reference Implementation)](#phase-1-header-template-reference-implementation)
4. [Phase 2: Modal Templates](#phase-2-modal-templates)
5. [Phase 3: Verification & Testing](#phase-3-verification--testing)
6. [Phase 4: Cleanup](#phase-4-cleanup)
7. [Troubleshooting](#troubleshooting)
8. [Success Criteria](#success-criteria)

---

## Current State Analysis

### What's Already Done ✅

- All `onclick` handlers converted to `@click` directives
- Functions registered with `Alpine.global()` in TypeScript
- 18 templates have `x-data="namespace"` attributes
- HTMX integration working for forms

### What's Still Missing ❌

**Problem**: 121 instances of manual DOM manipulation in TypeScript files

**Example from header.ts:7-17:**
```typescript
const toggleThemeDropdown = (): void => {
  const dropdown = document.getElementById("theme-dropdown");
  if (dropdown) {
    dropdown.classList.toggle("hidden"); // ← Manual DOM manipulation!
    const userMenu = document.getElementById("user-menu");
    if (userMenu && !dropdown.classList.contains("hidden")) {
      userMenu.classList.add("hidden"); // ← Manual DOM manipulation!
    }
  }
};
```

**Templates still using:**
- `id="theme-dropdown"` + `class="hidden"` for show/hide
- No reactive state variables
- No `x-show` directives
- No `@click.outside` for closing dropdowns
- No `x-transition` for animations

### What Needs Migration

**8 stateful templates** (modals, dropdowns, wizards):

1. ✅ **header.templ** - Theme dropdown + user menu (P0 - used in 17 places)
2. ✅ **collection_modal.templ** - Create/edit collection modal
3. ✅ **collections.templ** - Add books modal + navigation
4. ✅ **conflicts.templ** - Conflict resolution modal
5. ✅ **queue.templ** - Queue actions modal
6. ✅ **admin.templ** - Scan progress modal
7. ✅ **devices.templ** - Device token modal
8. ✅ **profile_modal.templ** - Profile edit modal

**Note**: Simple buttons with `@click` handlers are fine - no migration needed.

---

## Migration Strategy

### The Pattern

Every migration follows the same 4-step pattern:

1. **Template Changes**: Add `x-data` state, replace `class="hidden"` with `x-show`, add transitions
2. **TypeScript Cleanup**: Remove manual DOM manipulation functions
3. **Alpine Registration**: Remove deleted functions from `Alpine.global()`
4. **Testing**: Verify functionality, build, check for regressions

### Key Principles

- **State lives in template** (`x-data="{ open: false }"`)
- **UI updates automatically** (`x-show="open"`)
- **No manual DOM manipulation** in TypeScript
- **Pure business logic only** in TypeScript functions

---

## Phase 1: Header Template (Reference Implementation)

**Priority**: P0 (highest - used in 17 templates)
**Time**: 2-3 hours
**Complexity**: High (2 dropdowns + theme switching + click-outside)

### Step 1.1: Update header.templ

**Location**: `templates/header.templ`
**Lines to modify**: 46-191

**Current Structure (lines 46-53):**
```templ
<div class="relative">
  <button @click="toggleThemeDropdown()" class="p-2 rounded-lg hover:bg-gray-700 transition-colors" style="background-color: var(--bg-primary);">
    <svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"></path>
    </svg>
  </button>
  <div id="theme-dropdown" class="hidden absolute right-0 mt-2 w-64 rounded-lg shadow-lg z-50" style="background-color: var(--bg-secondary); border: 1px solid var(--border);">
```

**New Structure:**
```templ
<div x-data="{ themeDropdownOpen: false, userMenuOpen: false }">
  <!-- Theme Switcher -->
  <div class="relative">
    <button @click="themeDropdownOpen = !themeDropdownOpen"
            class="p-2 rounded-lg hover:bg-gray-700 transition-colors"
            style="background-color: var(--bg-primary);">
      <svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"></path>
      </svg>
    </button>
    <div x-show="themeDropdownOpen"
         @click.outside="themeDropdownOpen = false"
         x-transition:enter="transition ease-out duration-200"
         x-transition:enter-start="opacity-0 scale-95"
         x-transition:enter-end="opacity-100 scale-100"
         x-transition:leave="transition ease-in duration-150"
         x-transition:leave-start="opacity-100 scale-100"
         x-transition:leave-end="opacity-0 scale-95"
         class="absolute right-0 mt-2 w-64 rounded-lg shadow-lg z-50"
         style="background-color: var(--bg-secondary); border: 1px solid var(--border); display: none;">
```

**Key Changes:**
1. ✅ Wrapped both dropdowns in single `x-data` container (line 1)
2. ✅ Replaced `@click="toggleThemeDropdown()"` with `@click="themeDropdownOpen = !themeDropdownOpen"` (line 4)
3. ✅ Replaced `id="theme-dropdown" class="hidden"` with `x-show="themeDropdownOpen"` (line 13)
4. ✅ Added `@click.outside="themeDropdownOpen = false"` (line 14)
5. ✅ Added `x-transition` directives for smooth animations (lines 15-20)
6. ✅ Added `style="display: none;"` to prevent flash of unstyled content (line 22)

**Update theme buttons (lines 56, 60, 64, 68, 72, 76, 80):**

Replace:
```templ
<button @click="changeThemeTo('tokyo-night')" class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity" style="color: var(--text-primary); background-color: var(--bg-primary);">
```

With:
```templ
<button @click="header.changeThemeTo('tokyo-night'); themeDropdownOpen = false"
        class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
        style="color: var(--text-primary); background-color: var(--bg-primary);">
```

**Key Changes:**
1. ✅ Namespace added: `header.changeThemeTo('tokyo-night')`
2. ✅ Close dropdown after selection: `themeDropdownOpen = false`

**Update wood paneling buttons (lines 87, 96, 108, 120):**

Replace:
```templ
<button @click="changeWoodPaneling('none')" class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity" style="color: var(--text-primary);" data-wood="none">
```

With:
```templ
<button @click="woodPaneling.change('none'); themeDropdownOpen = false"
        class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
        style="color: var(--text-primary);"
        data-wood="none">
```

**Note**: We'll need to add `woodPaneling` namespace in Alpine registration (Step 1.3).

**Update user menu section (lines 136-160):**

Replace:
```templ
<div class="relative">
  <button @click="toggleUserMenu()" class="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-700 transition-colors" style="background-color: var(--bg-primary);">
    <svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
    </svg>
    <span class="text-sm hidden sm:block" style="color: var(--text-primary)">{ user.Username }</span>
  </button>
  <div id="user-menu" class="hidden absolute right-0 mt-2 w-48 rounded-lg shadow-lg z-50" style="background-color: var(--bg-secondary); border: 1px solid var(--border);">
```

With:
```templ
<div class="relative">
  <button @click="userMenuOpen = !userMenuOpen"
          class="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-700 transition-colors"
          style="background-color: var(--bg-primary);">
    <svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
    </svg>
    <span class="text-sm hidden sm:block" style="color: var(--text-primary)">{ user.Username }</span>
  </button>
  <div x-show="userMenuOpen"
       @click.outside="userMenuOpen = false"
       x-transition
       class="absolute right-0 mt-2 w-48 rounded-lg shadow-lg z-50"
       style="background-color: var(--bg-secondary); border: 1px solid var(--border); display: none;">
```

**Update logout button (line 156):**

Replace:
```templ
<button @click="logout()" class="block w-full text-left px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary);">
  Logout
</button>
```

With:
```templ
<button @click="header.logout(); userMenuOpen = false"
        class="block w-full text-left px-4 py-2 text-sm hover:opacity-80"
        style="color: var(--text-primary); background-color: var(--bg-secondary);">
  Logout
</button>
```

**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
<div x-data="collections" class="fixed inset-0 z-50 flex items-center justify-center bg-black/70">
```

**No change needed** - `x-data="collections"` namespace is correct, but we need local state.

**Add local state to wrapper:**

Replace line 4:
```templ
<div x-data="{ colorModalOpen: false }" x-data="collections" class="fixed inset-0 z-50 flex items-center justify-center bg-black/70">
```

**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
<div id="add-books-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
  <div class="card rounded-lg p-6 w-full max-w-2xl mx-4 my-8" style="background-color: var(--bg-secondary); border-color: var(--border);">
    <div class="flex justify-between items-center mb-6">
      <h2 class="text-xl font-bold" style="color: var(--text-primary)">Add Books to Collection</h2>
      <button @click="hideAddBooksModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
    </div>
```

**Replace with:**
```templ
<div x-data="{ addBooksModalOpen: false }">
  <!-- Button to open modal (line 156) -->
  <button @click="addBooksModalOpen = true" class="btn-primary px-4 py-2 rounded-lg">
    ➕ Add Books
  </button>

  <!-- Modal (line 227) -->
  <div x-show="addBooksModalOpen"
       x-transition
       @click.self="addBooksModalOpen = false"
       class="fixed inset-0 z-50 flex items-center justify-center"
       style="background-color: rgba(0, 0, 0, 0.7); display: none;">
    <div @click.stop
         class="card rounded-lg p-6 w-full max-w-2xl mx-4 my-8"
         style="background-color: var(--bg-secondary); border-color: var(--border);">
      <div class="flex justify-between items-center mb-6">
        <h2 class="text-xl font-bold" style="color: var(--text-primary)">Add Books to Collection</h2>
        <button @click="addBooksModalOpen = false"
                class="p-2 hover:opacity-80 rounded"
                style="color: var(--text-primary)">✕</button>
      </div>
```

**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
<!-- Button (line 156) -->
<button @click="$store.modals.showAddBooks()" class="btn-primary px-4 py-2 rounded-lg">
  ➕ Add Books
</button>

<!-- Modal (line 227) -->
<div x-show="$store.modals.addBooks"
     x-transition
     @click.self="$store.modals.hideAddBooks()"
     style="background-color: rgba(0, 0, 0, 0.7); display: none;">
```

**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
<div id="conflict-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto" style="background-color: rgba(0, 0, 0, 0.7);">
```

**Replace with:**
```templ
<div x-show="$store.modals.resolveConflict"
     x-transition
     @click.self="$store.modals.hideResolveConflict()"
     class="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
     style="background-color: rgba(0, 0, 0, 0.7); display: none;">
```

**Update button (line 110):**

Replace:
```templ
<button @click="showConflictModal('{ conflict.ID }')" class="btn-primary px-4 py-2 rounded-lg">
```

With:
```templ
<button @click="conflicts.showResolve('{ conflict.ID }'); $store.modals.showResolveConflict()"
        class="btn-primary px-4 py-2 rounded-lg">
```

**Remove from TypeScript:**
Delete `showConflictModal()`, `hideConflictModal()` functions.

### 2.4: queue.templ

**Location**: `templates/queue.templ`
**Time**: 1 hour
**Complexity**: Low (simple action modals)

**Use Alpine.store pattern** - same as conflicts.templ

### 2.5: admin.templ

**Location**: `templates/admin.templ`
**Time**: 1 hour
**Complexity**: Medium (scan progress modal)

**Use Alpine.store pattern** - same as collections.templ

### 2.6: devices.templ

**Location**: `templates/devices.templ`
**Time**: 1 hour
**Complexity**: Low (token generation modal)

**Use Alpine.store pattern** - same as conflicts.templ

### 2.7: profile_modal.templ

**Location**: `templates/profile_modal.templ`
**Time**: 1 hour
**Complexity**: Low (edit profile modal)

**Use Alpine.store pattern** - same as conflicts.templ

---

## Phase 3: Verification & Testing

### For Each Migrated Template

#### Build Verification

```bash
# 1. Build TypeScript
npm run build:ts

# 2. Generate templates
templ generate

# 3. Check for errors
# Expected: "Complete" with 0 errors
```

#### Manual Testing Checklist

**For header.templ:**
- [ ] Theme dropdown opens with smooth transition
- [ ] Theme dropdown closes when clicking outside
- [ ] Theme changes when option clicked
- [ ] Theme dropdown closes after selection
- [ ] User menu opens with smooth transition
- [ ] User menu closes when clicking outside
- [ ] Logout works and closes menu
- [ ] Wood paneling changes work
- [ ] Both dropdowns don't open simultaneously (mutual exclusion)

**For each modal template:**
- [ ] Modal opens with smooth transition
- [ ] Modal closes when clicking outside
- [ ] Modal closes when clicking X button
- [ ] Modal closes when clicking Cancel button
- [ ] Form submission works (HTMX)
- [ ] No console errors

#### Regression Testing

After each template migration:

```bash
# 1. Start application
go run .

# 2. Test all pages
- [ ] Dashboard loads
- [ ] Library loads
- [ ] Collections loads
- [ ] Devices loads
- [ ] Admin loads (if admin)

# 3. Test HTMX functionality
- [ ] Forms submit correctly
- [ ] Toast notifications appear
- [ ] Page updates work

# 4. Check browser console
- [ ] No JavaScript errors
- [ ] No 404s for .js files
- [ ] Alpine is loaded (check: window.Alpine in console)
```

---

## Phase 4: Cleanup

### 4.1: Remove Unused Functions

After all templates are migrated, search for remaining manual DOM manipulation:

```bash
cd /home/nymusicman/Code/bookhoard/web/src

# Find remaining classList manipulation
grep -rn "classList.toggle\|classList.add\|classList.remove" *.ts

# Find remaining getElementById for show/hide
grep -rn "getElementById.*dropdown\|getElementById.*modal" *.ts

# Find remaining event listeners for click-outside
grep -rn "addEventListener.*click" *.ts
```

**Expected**: Only 0-5 instances remaining (legitimate use cases)

### 4.2: Update ESBUILD_MIGRATION_PLAN.md

Add completion note:

```markdown
## Migration Status

### ✅ Completed
- Phase 0: ES Module Exports
- Phase 1: Internal TypeScript Dependencies
- Phase 2: Dual Exports (Alpine Bridge)
- Phase 3: Template Migration (ALL templates)

### 🎯 Final State
- 0 manual DOM manipulations (down from 121)
- All stateful templates using x-data/x-show
- Smooth transitions with x-transition
- Click-outside handling with @click.outside
- 60% less TypeScript code (header.ts: 100 → 40 lines)
```

### 4.3: Create Migration Documentation

Create `docs/contributing/alpinejs-patterns.md`:

```markdown
# Alpine.js Patterns in Bookhoard

## Modal Pattern

For modals loaded via HTMX, use Alpine.store:

```typescript
// In TypeScript
Alpine.store('modals', {
  myModal: false,
  show() { this.myModal = true; },
  hide() { this.myModal = false; }
});
```

```templ
<!-- In template -->
<button @click="$store.modals.show()">Open</button>
<div x-show="$store.modals.myModal"
     @click.self="$store.modals.hide()"
     x-transition>
  <!-- Modal content -->
</div>
```

## Dropdown Pattern

For dropdowns in same component, use local x-data:

```templ
<div x-data="{ dropdownOpen: false }">
  <button @click="dropdownOpen = !dropdownOpen">Toggle</button>
  <div x-show="dropdownOpen"
       @click.outside="dropdownOpen = false"
       x-transition>
    <!-- Dropdown content -->
  </div>
</div>
```

## Best Practices

- ✅ State lives in template (x-data)
- ✅ UI updates automatically (x-show)
- ✅ No manual DOM manipulation in TypeScript
- ✅ Pure business logic only in TypeScript functions
- ✅ Use Alpine.store for global state
- ✅ Use local x-data for component state
- ❌ Never use classList in TypeScript
- ❌ Never use getElementById for show/hide
```

---

## Troubleshooting

### Issue: Dropdown doesn't open

**Symptoms**: Click button, nothing happens

**Diagnosis**:
```javascript
// Open browser console
console.log(window.Alpine); // Should be defined

// Check if x-data is present
// Inspect element - should have x-data attribute
```

**Solutions**:
1. Check if `x-data` is on parent container
2. Check if Alpine is loaded: `<script src="/static/main.js"></script>`
3. Check for JavaScript errors in console
4. Verify variable name matches: `x-data="{ open: false }"` and `x-show="open"`

### Issue: Dropdown doesn't close when clicking outside

**Symptoms**: Click outside, dropdown stays open

**Solutions**:
1. Check if `@click.outside` is on dropdown div (not button)
2. Check for z-index conflicts
3. Check if other elements are blocking clicks

### Issue: No smooth transition

**Symptoms**: Dropdown appears instantly without animation

**Solutions**:
1. Check if `x-transition` is present
2. Check if `display: none;` is inline style (prevents FOUC)
3. Check for CSS conflicts (transition properties)

### Issue: Flash of unstyled content (FOUC)

**Symptoms**: Dropdown briefly visible before Alpine loads

**Solutions**:
1. Add inline style: `style="display: none;"`
2. Alpine will override this when it loads
3. Combine with `x-show` for reactive behavior

### Issue: Modal closes when clicking inside

**Symptoms**: Click inside modal, it closes

**Solutions**:
1. Use `@click.self` instead of `@click.outside`
2. Add `@click.stop` on inner elements to stop propagation
3. Check modal structure - ensure backdrop has click handler, not content

### Issue: TypeScript compilation error

**Symptoms**: `npm run build:ts` fails after changes

**Solutions**:
1. Check for undefined functions (removed but still referenced)
2. Check Alpine.global() registration (remove deleted functions)
3. Run `npm run build:ts -- --verbose` for detailed error

---

## Success Criteria

### Phase Completion Checklist

- [ ] All 8 stateful templates migrated
- [ ] All manual DOM manipulation removed (121 → 0 instances)
- [ ] All templates build successfully: `templ generate`
- [ ] TypeScript compiles successfully: `npm run build:ts`
- [ ] All dropdowns work with smooth transitions
- [ ] All modals work with smooth transitions
- [ ] Click-outside behavior works for all dropdowns/modals
- [ ] No console errors on any page
- [ ] No regressions in HTMX functionality
- [ ] All forms submit correctly
- [ ] Alpine DevTools shows reactive state correctly

### Metrics

**Before Migration:**
- 121 manual DOM manipulations
- 100 lines in header.ts
- No reactive state
- No smooth transitions
- Manual click-outside handling

**After Migration:**
- 0 manual DOM manipulations
- 40 lines in header.ts (60% reduction)
- Full reactive state with x-data
- Smooth transitions with x-transition
- Built-in click-outside with @click.outside

### Final Verification

```bash
# 1. Complete build
npm run build:ts
templ generate
go build

# 2. Check for remaining manual DOM manipulation
cd web/src
grep -rn "classList.toggle\|classList.add.*hidden\|classList.remove.*hidden" *.ts | wc -l
# Expected: 0-5 (legitimate use cases only)

# 3. Run application
go run .

# 4. Test all pages
# - Navigate through all major pages
# - Test all dropdowns
# - Test all modals
# - Test all forms

# 5. Check Alpine DevTools
# - Install browser extension
# - Inspect reactive state
# - Verify state changes correctly
```

---

## Appendix: Complete Code Examples

### A.1: Complete header.templ (After Migration)

```templ
package templates

templ Header(user User, currentPath string) {
	<nav class="border-b header-nav" style="border-color: var(--border); background-color: var(--bg-secondary);">
		<div class="w-full px-4 sm:px-6 lg:px-8">
			<div class="flex justify-between items-center h-16">
				<!-- Left: App Title & Navigation -->
				<div class="flex items-center space-x-6">
					<a href="/dashboard" class="text-xl font-bold hover:opacity-80 transition-opacity" style="color: var(--text-primary); text-decoration: none;">
						📚 Bookhoard
					</a>
					<div class="hidden md:flex items-center space-x-4">
						<a href="/dashboard" class="text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
							Library
						</a>
						<a href="/collections" class="text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
							Collections
						</a>
						<a href="/progress" class="text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
							Progress
						</a>
						<a href="/devices" class="text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
							Devices
						</a>
					</div>
				</div>
				<!-- Center: Search Box -->
				<div class="flex-1 max-w-2xl mx-8">
					<div class="relative">
						<input
							type="text"
							id="header-search"
							placeholder="Search your library..."
							class="w-full px-4 py-2 pl-10 border rounded-lg"
							style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
							autocomplete="off"
						/>
						<svg class="absolute left-3 top-2.5 h-5 w-5" style="color: var(--text-secondary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
							<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
						</svg>
					</div>
				</div>
				<!-- Right: Theme Switcher & User Menu -->
				<div x-data="{ themeDropdownOpen: false, userMenuOpen: false }">
					<!-- Theme Switcher -->
					<div class="relative">
						<button @click="themeDropdownOpen = !themeDropdownOpen"
										class="p-2 rounded-lg hover:bg-gray-700 transition-colors"
										style="background-color: var(--bg-primary);">
							<svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
								<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"></path>
							</svg>
						</button>
						<div x-show="themeDropdownOpen"
								 @click.outside="themeDropdownOpen = false"
								 x-transition:enter="transition ease-out duration-200"
								 x-transition:enter-start="opacity-0 scale-95"
								 x-transition:enter-end="opacity-100 scale-100"
								 x-transition:leave="transition ease-in duration-150"
								 x-transition:leave-start="opacity-100 scale-100"
								 x-transition:leave-end="opacity-0 scale-95"
									class="absolute right-0 mt-2 w-64 rounded-lg shadow-lg z-50"
									style="background-color: var(--bg-secondary); border: 1px solid var(--border); display: none;">
							<div class="p-4">
								<h3 class="text-sm font-semibold mb-3" style="color: var(--text-primary)">Select Theme</h3>
								<div class="space-y-2">
									<button @click="header.changeThemeTo('tokyo-night'); themeDropdownOpen = false"
													class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
													style="color: var(--text-primary); background-color: var(--bg-primary);">
										<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #7aa2f7;"></span>
										Tokyo Night
									</button>
									<button @click="header.changeThemeTo('dracula'); themeDropdownOpen = false"
													class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
													style="color: var(--text-primary); background-color: var(--bg-primary);">
										<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #bd93f9;"></span>
										Dracula
									</button>
									<button @click="header.changeThemeTo('nord'); themeDropdownOpen = false"
													class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
													style="color: var(--text-primary); background-color: var(--bg-primary);">
										<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #88c0d0;"></span>
										Nord
									</button>
									<button @click="header.changeThemeTo('solarized-dark'); themeDropdownOpen = false"
													class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
													style="color: var(--text-primary); background-color: var(--bg-primary);">
										<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #2aa198;"></span>
										Solarized Dark
									</button>
									<button @click="header.changeThemeTo('monokai'); themeDropdownOpen = false"
													class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
													style="color: var(--text-primary); background-color: var(--bg-primary);">
										<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #a6e22e;"></span>
										Monokai
									</button>
									<button @click="header.changeThemeTo('one-dark-pro'); themeDropdownOpen = false"
													class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
													style="color: var(--text-primary); background-color: var(--bg-primary);">
										<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #61dafb;"></span>
										One Dark Pro
									</button>
									<button @click="header.changeThemeTo('material-dark'); themeDropdownOpen = false"
													class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
													style="color: var(--text-primary); background-color: var(--bg-primary);">
										<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #80cbc4;"></span>
										Material Dark
									</button>
									<div class="border-t pt-2 mt-2" style="border-color: var(--border);">
										<p class="text-xs mb-2" style="color: var(--text-secondary)">Bookshelf Background</p>
										<button @click="woodPaneling.change('none'); themeDropdownOpen = false"
														class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
														style="color: var(--text-primary);"
														data-wood="none">
											None
										</button>
										<button @click="woodPaneling.change('wood-light'); themeDropdownOpen = false"
														class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
														style="color: var(--text-primary);"
														data-wood="wood-light">
											<span class="inline-block w-4 h-4 rounded mr-2"
														style="background: url('/static/textures/wood-light.png'); background-size: cover;"></span>
											Wood Light
										</button>
										<button @click="woodPaneling.change('wood-dark'); themeDropdownOpen = false"
														class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
														style="color: var(--text-primary);"
														data-wood="wood-dark">
											<span class="inline-block w-4 h-4 rounded mr-2"
														style="background: url('/static/textures/wood-dark.png'); background-size: cover;"></span>
											Wood Dark
										</button>
										<button @click="woodPaneling.change('wood-mahogany'); themeDropdownOpen = false"
														class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
														style="color: var(--text-primary);"
														data-wood="wood-mahogany">
											<span class="inline-block w-4 h-4 rounded mr-2"
														style="background: url('/static/textures/wood-mahogany.png'); background-size: cover;"></span>
											Wood Mahogany
										</button>
									</div>
								</div>
							</div>
						</div>
					</div>
					<!-- User Icon with Dropdown -->
					<div class="relative">
						if user.ID != "" {
							<!-- LOGGED IN: Show user menu with logout -->
							<button @click="userMenuOpen = !userMenuOpen"
											class="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-700 transition-colors"
											style="background-color: var(--bg-primary);">
								<svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
									<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
								</svg>
								<span class="text-sm hidden sm:block" style="color: var(--text-primary)">{ user.Username }</span>
							</button>
							<div x-show="userMenuOpen"
									 @click.outside="userMenuOpen = false"
									 x-transition
									 class="absolute right-0 mt-2 w-48 rounded-lg shadow-lg z-50"
									 style="background-color: var(--bg-secondary); border: 1px solid var(--border); display: none;">
								<div class="py-1">
									<a href="/profile" class="block px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary); text-decoration: none;">
										Profile
									</a>
									if user.Role == "admin" {
										<a href="/admin" class="block px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary); text-decoration: none;">
											Admin Panel
										</a>
									}
									<div class="border-t my-1" style="border-color: var(--border);"></div>
									<button @click="header.logout(); userMenuOpen = false"
													class="block w-full text-left px-4 py-2 text-sm hover:opacity-80"
													style="color: var(--text-primary); background-color: var(--bg-secondary);">
										Logout
									</button>
								</div>
							</div>
						} else {
							<!-- LOGGED OUT: Show login form -->
							<button @click="userMenuOpen = !userMenuOpen"
											class="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-700 transition-colors"
											style="background-color: var(--bg-primary);">
								<svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
									<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
								</svg>
								<span class="text-sm hidden sm:block" style="color: var(--text-primary)">Login</span>
							</button>
							<div x-show="userMenuOpen"
									 @click.outside="userMenuOpen = false"
									 x-transition
									 class="absolute right-0 mt-2 w-64 rounded-lg shadow-lg z-50 p-4"
									 style="background-color: var(--bg-secondary); border: 1px solid var(--border); display: none;">
								<form hx-post="/api/auth/login" hx-target="#login-result" hx-swap="innerHTML" class="space-y-3">
									<input type="hidden" name="redirect" value="{ currentPath }"/>
									<div>
										<label class="block text-sm mb-1" style="color: var(--text-secondary)">Email or Username</label>
										<input type="text" name="login" class="w-full px-3 py-2 border rounded text-sm" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" required/>
									</div>
									<div>
										<label class="block text-sm mb-1" style="color: var(--text-secondary)">Password</label>
										<input type="password" name="password" class="w-full px-3 py-2 border rounded text-sm" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" required/>
									</div>
									<button type="submit" class="w-full py-2 rounded text-sm" style="background-color: var(--accent); color: white;">
										Sign In
									</button>
								</form>
								<div id="login-result"></div>
								<div class="border-t my-2" style="border-color: var(--border);"></div>
								<a href="/register" class="block text-center text-sm hover:opacity-80" style="color: var(--text-secondary); text-decoration: none;">
									Don't have an account? Sign Up
								</a>
							</div>
						}
					</div>
				</div>
			</div>
		</div>
	</nav>
	<script src="/static/main.js"></script>
}
```

### A.2: Complete header.ts (After Migration)

```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();
  },
});
```

### A.3: Alpine.store Pattern for Modals

**TypeScript (web/src/collections.ts):**
```typescript
import { Alpine } from "./alpine";

// Create global modal store
Alpine.store('modals', {
  addBooks: false,
  editCollection: false,

  showAddBooks() {
    this.addBooks = true;
  },

  hideAddBooks() {
    this.addBooks = false;
  },

  showEditCollection() {
    this.editCollection = true;
  },

  hideEditCollection() {
    this.editCollection = false;
  }
});

// Business logic functions (no DOM manipulation)
async function addSelectedBooks() {
  const token = localStorage.getItem("token");
  // ... API call logic ...
}

async function searchBooksForCollections(query: string) {
  // ... search logic ...
}

export { addSelectedBooks, searchBooksForCollections };
```

**Template (templates/collections.templ):**
```templ
<!-- Button to open modal -->
<button @click="$store.modals.showAddBooks()" class="btn-primary">
  ➕ Add Books
</button>

<!-- Modal -->
<div x-show="$store.modals.addBooks"
     x-transition
     @click.self="$store.modals.hideAddBooks()"
     class="fixed inset-0 z-50 flex items-center justify-center"
     style="background-color: rgba(0, 0, 0, 0.7); display: none;">

  <div @click.stop class="card rounded-lg p-6">
    <div class="flex justify-between items-center mb-6">
      <h2>Add Books to Collection</h2>
      <button @click="$store.modals.hideAddBooks()">✕</button>
    </div>

    <!-- Modal content -->
    <input type="text" placeholder="Search books...">
    <button @click="addSelectedBooks()">Add Selected</button>
  </div>
</div>
```

---

## Next Steps

1. **Start with header.templ migration** (highest priority, reference implementation)
2. **Apply Alpine.store pattern** to all modal templates
3. **Test thoroughly** after each migration
4. **Clean up unused functions** from TypeScript files
5. **Update documentation** with patterns learned
6. **Verify final state**: 0 manual DOM manipulations

**Estimated completion time**: 10-12 hours

**Success metrics**:
- ✅ All 8 templates migrated
- ✅ 121 manual DOM manipulations → 0
- ✅ All dropdowns/modals use reactive state
- ✅ Smooth transitions throughout
- ✅ Clean, maintainable codebase

Good luck with the migration! 🚀
