diff --git a/esbuild-setup.md b/esbuild-setup.md
index 2f5772a..3f6ad61 100644
--- a/esbuild-setup.md
+++ b/esbuild-setup.md
@@ -1,719 +1,1953 @@
-# ESBuild Setup and Migration Guide
+# ESBuild Setup Guide
## Overview
+Migrate to a single bundled `main.js` using ESBuild for better performance, simplified deployment, and broader browser compatibility (ES2020 target = Chrome 80+, Firefox 72+, Safari 13.1+, Edge 80+).
-This guide walks through setting up ESBuild properly for Bookhoard, eliminating `(window as any)` globals, and following project guidelines (procedural TypeScript, SSR-first, progressive enhancement).
-
-**Target**: Single `main.js` bundle (~100KB minified) with ES2020 support for broad browser compatibility.
+**Target Bundle Size**: ~100KB minified + gzip
+**Browser Support**: ES2020 (Chrome 80+, Firefox 72+, Safari 13.1+, Edge 80+, Ubuntu 22.04 LTS browsers)
+**Architecture**: SSR-first, procedural TypeScript, progressive enhancement (per PROJECT_GUIDELINES.md)
---
-## Table of Contents
+## Status: What's Already Done ✅
-1. [Current State Assessment](#current-state-assessment)
-2. [ESBuild Configuration](#esbuild-configuration)
-3. [Package.json Setup](#packagejson-setup)
-4. [Removing Globals](#removing-globals)
-5. [Import Strategy](#import-strategy)
-6. [Template Updates](#template-updates)
-7. [Build and Test](#build-and-test)
-8. [Verification Checklist](#verification-checklist)
+### 1. package.json Dependencies (Already Correct)
+**File**: `package.json`
+**Status**: ✅ Already configured - NO CHANGES NEEDED
----
+Dependencies (lines 14-26):
+- `htmx.org`, `alpinejs`, `lunr`, `highlight.js` already in dependencies
+- `esbuild`, `typescript`, TailwindCSS tooling already in devDependencies
-## Current State Assessment
-
-### What We Have
-
-- **29 TypeScript files** in `web/src/` (no npm imports currently)
-- **Existing `main.ts`** that imports all modules (good foundation!)
-- **Separate unbundled .js files** in `web/static/` (causes issues)
-- **404 errors** for htmx.min.js, lunr.min.js, highlight.min.js (not copied from node_modules)
-- **`(window as any)` globals** in docs.ts (antipattern with bundler)
-
-### What We Need
-
-- ✅ Single `main.js` bundle from `web/src/main.ts`
-- ✅ ESBuild minification with `--target=es2020`
-- ✅ Direct imports of npm packages (no globals)
-- ✅ Keep imports where used (don't overwhelm main.ts)
-- ✅ Progressive enhancement (pages work without JS)
-
----
-
-## ESBuild Configuration
-
-### Why ESBuild?
-
-- **Go-based** (matches templ/sqlc philosophy)
-- **10-100x faster** than Webpack
-- **Small node_modules** (~6MB vs 44MB with Vite)
-- **Automatic tree-shaking and minification**
-- **Single-pass compilation** (no separate TypeScript step needed)
-
-### ES2020 Target
+### 2. Build Scripts (Already Correct)
+**File**: `package.json`
+**Status**: ✅ Already configured - NO CHANGES NEEDED
+Lines 8-10:
```json
-"--target=es2020"
+"build:ts": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020 --minify",
+"watch:ts": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020",
+"dev": "concurrently \"npm run watch:css\" \"npm run watch:ts\" \"npm run watch:templ\"",
```
-**Why not esnext?**
-- ES2020 supports browsers from 2020+ (Chrome 80+, Firefox 72+, Safari 13.1+)
-- Covers Ubuntu 22.04 LTS users (supported until 2027)
-- Covers lightweight browsers (Ephiphany, Falkon with recent Qt/WebKit)
-- Avoids bug reports from users with slightly older browsers
+Already using `--target=es2020` for broad browser compatibility.
+
+### 3. Entry Point (Already Exists)
+**File**: `web/src/main.ts`
+**Status**: ✅ Already exists - NO CHANGES NEEDED
+
+Already imports all 29 modules correctly (24 lines). Main.ts stays simple - imports happen in individual files where used.
---
-## package.json Setup
+## Phase 1: Clean Up docs.ts
-### Current State (Already Correct!)
+**File**: `web/src/docs.ts` (99 lines)
+**Status**: Imports already added ✅
+**Lines to modify**: 50, 56, 73 (remove window globals), 99 (replace with Alpine)
-Your `package.json` already has perfect dependency classification:
+**Note**: Lines 3-4 already have the correct imports:
+```typescript
+import * as lunr from "lunr";
+import hljs from "highlight.js";
+```
-```json
-{
- "dependencies": {
- "htmx.org": "^2.0.8",
- "alpinejs": "^3.15.8",
- "lunr": "^2.3.9",
- "highlight.js": "^11.11.1"
- },
- "devDependencies": {
- "esbuild": "^0.27.3",
- "typescript": "^5.9.3",
- "@tailwindcss/forms": "^0.5.11",
- "@tailwindcss/typography": "^0.5.19",
- "autoprefixer": "^10.4.27",
- "tailwindcss": "^3.4.19"
+### Step 1: Remove (window as any).lunr check
+**Location**: Line 50
+
+**Current** (lines 49-53):
+```typescript
+ if (!(window as any).lunr) {
+ console.warn("Lunr.js not loaded");
+ return;
}
-}
```
-**Runtime dependencies** (bundled into main.js):
-- ✅ htmx.org - Declarative AJAX framework
-- ✅ alpinejs - Reactive UI components
-- ✅ lunr - Full-text search
-- ✅ highlight.js - Syntax highlighting
-
-**Build-time dependencies**:
-- ✅ esbuild - Bundler
-- ✅ typescript - Type checker
-- ✅ tailwindcss/* - CSS tooling
-
-### Update Build Scripts
-
-Replace the current build scripts in `package.json`:
-
-```json
-{
- "scripts": {
- "build:css": "tailwindcss -i ./web/static/input.css -o ./web/static/style.css --watch",
- "build:css:prod": "tailwindcss -i ./web/static/input.css -o ./web/static/style.css --minify",
- "build:ts": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020 --minify",
- "build:ts:dev": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020",
- "build:ts:watch": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020 --watch",
- "build": "npm run build:ts && npm run build:css:prod",
- "dev": "npm run build:ts:dev && npm run build:css"
- }
-}
-```
-
-**Key changes**:
-- ✅ Bundle from `web/src/main.ts` (single entry point)
-- ✅ Output to `web/static/main.js` (single file)
-- ✅ Keep `--target=es2020` (browser compatibility)
-- ✅ `--minify` for production builds
-- ✅ Added convenience scripts (`build`, `dev`)
-
----
-
-## Removing Globals
-
-### Current Global Usage
-
-**File**: `web/src/docs.ts`
-
+**Change to**:
```typescript
-// Line 45: Check if lunr loaded
-if (!(window as any).lunr) {
- console.warn("Lunr.js not loaded");
- return;
-}
-
-// Line 51: Access search index
-const idx = (window as any).lunrIndex;
-
-// Line 68: Access docs data
-const doc = (window as any).docsData?.[result.ref];
-
-// Line 94: Export function globally
-(window as any).toggleSidebar = toggleSidebar;
+ // Lunr now bundled via ESBuild
```
-### Why This Exists
-
-Your current setup loads libraries via separate `
-
-```
-
-This attaches libraries to `window`, requiring type casts to access.
-
-### Three Types of Globals
-
-#### Type 1: Library Imports (Should be direct imports)
-
-**Example**: `(window as any).lunr`
-
-**Fix**: Import directly in files that use it
+### Step 2: Replace (window as any).lunrIndex with direct lunr usage
+**Location**: Line 56
+**Current** (lines 55-62):
```typescript
-// web/src/docs.ts
-import lunr from 'lunr';
-
-// Use directly, no window needed
-function buildSearchIndex(data: any[]) {
- const idx = lunr(function() {
- this.use(lunr.flex);
- this.ref('id');
- this.field('title', {boost: 10});
- this.field('content', {boost: 1});
- data.forEach(doc => this.add(doc));
- });
- return idx;
-}
+ const idx = (window as any).lunrIndex;
+ if (!idx) {
+ searchResults.innerHTML =
+ '
Search index not loaded
';
+ searchResults.classList.remove("hidden");
+ return;
+ }
```
-#### Type 2: SSR Data (Server-side rendered data)
-
-**Example**: `(window as any).lunrIndex`, `(window as any).docsData`
-
-These are **NOT library globals** - they're data fetched from the server at runtime.
-
-**Current approach**: Template fetches `/docs/search-index.json` and builds lunr index
-
-**Three options**:
-
-**Option A: Keep as globals with proper types** (simplest)
-
+**Change to**:
```typescript
-// web/src/docs.ts
-// Add global declaration
-declare global {
- interface Window {
- lunrIndex: any;
- docsData: Record;
- }
-}
-
-// Use without type casts
-const idx = window.lunrIndex;
-const doc = window.docsData?.[result.ref];
+ const idx = lunr.Builder.loadJs(searchIndex);
+ if (!idx) {
+ searchResults.innerHTML =
+ 'Search index not loaded
';
+ searchResults.classList.remove("hidden");
+ return;
+ }
```
-**Option B: Data attributes** (cleaner, requires template changes)
-
-```go
-// In Go template
-
-```
+### Step 3: Replace (window as any).docsData with direct import
+**Location**: Line 73
+**Current** (lines 70-76):
```typescript
-// In TypeScript
-const container = document.getElementById('search-container')!;
-const searchIndex = JSON.parse(container.dataset.searchIndex!);
-const docsData = JSON.parse(container.dataset.docsData!);
+ .map((result: { ref: string }) => {
+ const doc = (window as any).docsData?.[result.ref];
+ if (!doc) return "";
```
-**Option C: Loader module** (best, most maintainable)
-
+**Change to**:
```typescript
-// web/src/search-data.ts (NEW FILE)
-let lunrIndex: any = null;
-let docsData: Record
= {};
-
-export async function loadSearchData() {
- const response = await fetch('/docs/search-index.json');
- const data = await response.json();
-
- // Build lunr index
- lunrIndex = lunr(function() {
- this.use(lunr.flex);
- this.ref('id');
- this.field('title', {boost: 10});
- this.field('content', {boost: 1});
- data.forEach(doc => this.add(doc));
- });
-
- docsData = data;
-}
-
-export { lunrIndex, docsData };
+ .map((result: { ref: string }) => {
+ const doc = docs[result.ref];
+ if (!doc) return "";
```
-```typescript
-// web/src/docs.ts
-import { lunrIndex, docsData, loadSearchData } from './search-data';
+### Step 4: Replace window export with Alpine global
+**Location**: Line 99
-document.addEventListener('DOMContentLoaded', async () => {
- await loadSearchData();
+**Current** (lines 98-99):
+```typescript
+document.addEventListener("DOMContentLoaded", () => {
initializeDocsSearch();
});
-function performDocsSearch(query: string) {
- // Use imported data, no globals
- const idx = lunrIndex;
- const doc = docsData[result.ref];
-}
+(window as any).toggleSidebar = toggleSidebar;
```
-#### Type 3: Function Exports for HTML (Should use event listeners)
-
-**Example**: `(window as any).toggleSidebar = toggleSidebar;`
-
-**Why**: Template uses `onclick="toggleSidebar()"`
-
-**Fix**: Replace `onclick` attributes with `addEventListener`
-
+**Change to**:
```typescript
-// Remove: (window as any).toggleSidebar = toggleSidebar;
-
-// Add event listener
-document.addEventListener('click', (e) => {
- const button = e.target.closest('[data-action="toggle-sidebar"]');
- if (button) toggleSidebar();
+document.addEventListener("DOMContentLoaded", () => {
+ initializeDocsSearch();
+
+ // Register with Alpine globally
+ if (typeof window.Alpine !== 'undefined') {
+ window.Alpine.effect(() => {
+ window.Alpine.global('docs', {
+ toggleSidebar
+ });
+ });
+ }
});
```
-```html
-
-
-```
-
-### Recommended Approach
-
-**Quick fix** (Types 1 + 2A):
-1. Import libraries directly (`import lunr from 'lunr'`)
-2. Add proper TypeScript declarations for SSR globals
-3. Keep function exports for now (can fix later)
-
-**Best practice** (All types):
-1. Import libraries directly
-2. Use Option C (loader module) for SSR data
-3. Replace all `onclick` with `addEventListener`
+**Why Alpine.global()**: Makes `toggleSidebar()` available to Alpine templates via `@click="docs.toggleSidebar()"`
---
-## Import Strategy
+## Phase 2: Migrate TypeScript Files to Alpine Registration
-### Principle: Import Where Used
+**Approach**: Replace `(window as any)` exports with Alpine.js global registration
-**❌ DON'T**: Import everything in main.ts
+### Architecture Note: Alpine.js for Client-Side State
+**Why Alpine over window exports**:
+- Modern, reactive framework (already in package.json)
+- Clean template syntax: `@click` instead of `onclick="window.func()"`
+- Built-in state management: `x-data`, `x-show`, `x-model`
+- Works with SSR (progressive enhancement)
+- No global namespace pollution
+
+**Hybrid approach**:
+- **Alpine**: Client-side state (modals, dropdowns, theme, forms)
+- **HTMX**: Server calls (already using for form submissions)
+
+### Step 1: Create Alpine Registration Helper
+
+**New file**: `web/src/alpine.ts`
```typescript
-// main.ts - DON'T DO THIS
-import './collections';
-import './docs';
-import 'htmx.org';
-import 'alpinejs';
-import 'lunr';
-import 'highlight.js/lib/common';
-// ... 29 more imports
-```
-
-This overwhelms main.ts and makes it hard to see what each file needs.
-
-**✅ DO**: Import in files that use it
-
-```typescript
-// main.ts - Keep simple!
-import './collections';
-import './toast';
-import './theme';
-// ... all page modules (29 total)
-```
-
-```typescript
-// web/src/collections.ts - Import what YOU use
import Alpine from 'alpinejs';
-// Use Alpine for checkbox state
-document.addEventListener('alpine:init', () => {
- Alpine.data('bookSelection', () => ({
- selectedBooks: new Set(),
- toggle(id: string) { /* ... */ }
- }));
+// Initialize Alpine
+window.Alpine = Alpine;
+Alpine.start();
+
+// Re-export Alpine for other modules to use
+export { Alpine };
+```
+
+**Add to main.ts**: Append this line at the end of `web/src/main.ts`:
+
+```typescript
+import './alpine';
+```
+
+### Step 2: Update TypeScript Files to Register with Alpine
+
+#### Pattern: Object Registration (Multiple Related Functions)
+
+**Example**: `web/src/toast.ts`
+
+**Current** (lines 229-236):
+```typescript
+(window as any).showToast = {
+ error: (message: string, duration?: number) =>
+ showToast(message, "error", duration),
+ success: (message: string, duration?: number) =>
+ showToast(message, "success", duration),
+ info: (message: string, duration?: number) =>
+ showToast(message, "info", duration),
+};
+```
+
+**Change to**:
+```typescript
+import { Alpine } from './alpine';
+
+// Register toast API with Alpine
+Alpine.global('showToast', {
+ error: (message: string, duration?: number) =>
+ showToast(message, "error", duration),
+ success: (message: string, duration?: number) =>
+ showToast(message, "success", duration),
+ info: (message: string, duration?: number) =>
+ showToast(message, "info", duration),
});
```
-```typescript
-// web/src/docs.ts - Import what YOU use
-import lunr from 'lunr';
-import hljs from 'highlight.js/lib/common';
-import 'highlight.js/lib/languages/bash';
-import 'highlight.js/lib/languages/go';
-import 'highlight.js/lib/languages/sql';
-import 'highlight.js/lib/languages/http';
-import 'highlight.js/lib/languages/json';
+**Usage in templates**:
+```html
+
+
+
-// Use lunr for search
-const idx = lunr(function() { /* ... */ });
-
-// Make hljs available globally (needed for docs template)
-if (typeof window !== 'undefined') {
- (window as any).hljs = hljs;
-}
+
+
+
```
-### ESBuild Handles the Rest
+#### Pattern: Namespace Registration (Grouping Related Functions)
-When you run `esbuild web/src/main.ts --bundle`:
+**Example**: `web/src/api.ts`
-1. Starts at `main.ts`
-2. Follows all imports through all modules
-3. Collects all dependencies
-4. Bundles everything into `main.js`
-5. Handles deduplication automatically
-6. Tree-shakes unused code
+**Current** (lines 90-99):
+```typescript
+(window as any).api = {
+ get: apiGet,
+ post: apiPost,
+ put: apiPut,
+ delete: apiDelete,
+ patch: apiPatch,
+ handleResponse,
+ handleVoidResponse,
+ handleError,
+};
+```
-**Result**: Single `main.js` with everything you need, nothing you don't.
+**Change to**:
+```typescript
+import { Alpine } from './alpine';
+
+Alpine.global('api', {
+ get: apiGet,
+ post: apiPost,
+ put: apiPut,
+ delete: apiDelete,
+ patch: apiPatch,
+ handleResponse,
+ handleVoidResponse,
+ handleError,
+});
+```
+
+**Usage in templates**:
+```html
+
+
+
+
+
+```
+
+#### Pattern: Stateful Components (Dropdowns, Modals)
+
+**Example**: `web/src/header.ts` (theme dropdown)
+
+**Current approach**: Multiple window exports
+
+**New approach**: Create Alpine component with state
+
+**Add to `web/src/header.ts`**:
+```typescript
+import { Alpine } from './alpine';
+
+// Register theme dropdown component
+Alpine.data('themeDropdown', () => ({
+ open: false,
+
+ toggle() {
+ this.open = !this.open;
+ },
+
+ changeTheme(theme: string) {
+ changeThemeTo(theme); // Reuse existing function
+ this.open = false;
+ },
+
+ init() {
+ // Load saved theme on init
+ applyTheme(loadTheme());
+ }
+}));
+```
+
+**Usage in templates**:
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Files Requiring Alpine Migration
+
+**High priority** (used by 10+ templates):
+1. `web/src/toast.ts` - Register `showToast`
+2. `web/src/api.ts` - Register `api` object
+3. `web/src/events.ts` - Register event functions
+4. `web/src/storage.ts` - Register storage helpers
+
+**Medium priority** (stateful components):
+5. `web/src/header.ts` - Create `themeDropdown` component
+6. `web/src/woodPaneling.ts` - Create `woodPaneling` component
+7. `web/src/collections.ts` - Register collection functions
+8. `web/src/conflicts.ts` - Register conflict functions
+9. `web/src/search.ts` - Register search functions
+10. `web/src/dom.ts` - Register DOM helpers
+
+**Lower priority** (page-specific):
+11. `web/src/dashboard.ts` - Dashboard-specific functions
+12. `web/src/devices.ts` - Device management functions
+13. `web/src/queue.ts` - Queue management functions
+14. `web/src/analytics.ts` - Analytics functions (Chart.js)
+15. `web/src/admin.ts` - Admin functions
+
+**Keep as-is for now** (already working):
+- Lunr/Highlight.js imports (docs.ts) - already handled
+- Chart.js usage - already loaded via CDN
---
-## Template Updates
+## Phase 3: Update Templates to Alpine Directives
-### Current State
+### Template Migration Strategy
-Templates have multiple script tags:
+**151 onclick handlers need migration** across 27 templates. Use this approach:
-```html
-
-
-
-
-
-
-
-
-
-
+1. **Add `x-data` component** to sections with state
+2. **Replace `onclick`** with `@click`
+3. **Replace `class="hidden"`** with `x-show="!open"`
+4. **Add transitions** with `x-transition`
+5. **Use `x-model`** for form inputs
+
+### Template Changes (27 files)
+
+---
+
+#### 1. templates/collections.templ
+**File**: `templates/collections.templ` (271 lines)
+
+**Remove script blocks** (lines 12-13, 111-112):
+DELETE all individual `
```
-### Update to Single Script Tag
-
-```html
-
-
-
-
-
-
-
-
+**Change to**:
+```templ
+
```
-### Files to Update
+**Update onclick handlers**:
-**1. collections.templ**
-- Remove ``
+
+
+```
-**3. All other templates** (25 files)
-- Replace all ``
+**Line 119** - Back button:
+```templ
+
+