From a84ffb253e38cb77c9516b2c908f5dee4edf1e60 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 8 Mar 2026 01:14:48 -0500 Subject: [PATCH] chore: Remove obsolete documentation and regenerate template Remove outdated migration documentation and regenerate template after script tag cleanup. Changes: - Delete ECHO_V5_MIGRATION.md: Obsolete migration plan, superseded by ESBUILD_MIGRATION_PLAN.md - Delete esbuild-setup.md: Incomplete setup document, replaced by comprehensive migration plan - Regenerate templates/collections_templ.go: Remove collections.js script tag (now using main.js bundle) Template update: - Removed -``` - -**Change to**: -```templ - -``` - -**Update onclick handlers**: - -**Line 63** - Collection card: -```templ - -
- - -
-``` - -**Line 119** - Back button: -```templ - - - -``` - -**Change to Alpine**: -```templ -
- - -
- -
- - - - - -
-
-
-``` - -**Update user menu** (lines 57-60, 65): -```templ - - -``` - -**Change to**: -```templ -
- -
-
-``` - ---- - -#### 7. templates/analytics.templ -**File**: `templates/analytics.templ` (100 lines) - -**Remove first script block** (lines 10-14): -```templ - - - - - -``` -DELETE these lines entirely. - -**Note**: Chart.js remains on CDN (intentionally not bundled - 3.4MB, only used on 1 page). - -**Replace analytics.js script** (line 97): -```templ - -``` - -**Change to**: -```templ - - -``` - ---- - -#### 8. templates/bookshelf.templ -**File**: `templates/bookshelf.templ` (266 lines) - -**Remove first script block** (lines 10-13): -```templ - - - - -``` -DELETE these lines entirely. - -**Replace bookshelf.js script** (line 63): -```templ - -``` - -**Change to**: -```templ - -``` - ---- - -#### 9. templates/devices.templ -**File**: `templates/devices.templ` (870 lines) - -**Replace script block** (lines 12-16): -```templ - - - - - -``` - -**Change to**: -```templ - -``` - ---- - -#### 10. templates/index.templ -**File**: `templates/index.templ` (165 lines) - -**Remove first script block** (lines 10-12): -```templ - - - -``` -DELETE these lines entirely. - -**Replace index.js script** (line 138): -```templ - -``` - -**Change to**: -```templ - -``` - ---- - -#### 11. templates/login.templ -**File**: `templates/login.templ` (81 lines) - -**Replace script block** (lines 10-12): -```templ - - - -``` - -**Change to**: -```templ - -``` - ---- - -#### 12. templates/profile.templ -**File**: `templates/profile.templ` (68 lines) - -**Replace script block** (lines 9-11): -```templ - - - -``` - -**Change to**: -```templ - -``` - ---- - -#### 13. templates/queue.templ -**File**: `templates/queue.templ` (169 lines) - -**Remove first script block** (lines 12-15): -```templ - - - - -``` -DELETE these lines entirely. - -**Replace queue.js script** (line 166): -```templ - -``` - -**Change to**: -```templ - -``` - ---- - -#### 14. templates/conflicts.templ -**File**: `templates/conflicts.templ` (239 lines) - -**Remove first script block** (lines 12-15): -```templ - - - - -``` -DELETE these lines entirely. - -**Replace conflicts.js script** (line 234): -```templ - -``` - -**Change to**: -```templ - -``` - ---- - -#### 15. templates/custom_section.templ -**File**: `templates/custom_section.templ` (168 lines) - -**Replace script block** (lines 10-13): -```templ - - - - -``` - -**Change to**: -```templ - -``` - ---- - -#### 16. templates/admin_users.templ -**File**: `templates/admin_users.templ` (145 lines) - -**Replace script block** (lines 9-12): -```templ - - - - -``` - -**Change to**: -```templ - -``` - ---- - -#### 17. templates/collection_rules.templ -**File**: `templates/collection_rules.templ` (420 lines) - -**Replace script block** (lines 10-12): -```templ - - - -``` - -**Change to**: -```templ - -``` - ---- - -#### 18. templates/progress.templ -**File**: `templates/progress.templ` (114 lines) - -**Replace script block** (lines 12-14): -```templ - - - -``` - -**Change to**: -```templ - -``` - ---- - -#### 19. templates/register.templ -**File**: `templates/register.templ` (97 lines) - -**Replace script block** (lines 10-12): -```templ - - - -``` - -**Change to**: -```templ - -``` - ---- - -#### 20. templates/settings.templ -**File**: `templates/settings.templ` (157 lines) - -**Replace script block** (lines 9-11): -```templ - - - -``` - -**Change to**: -```templ - -``` - ---- - -#### 21. templates/stats.templ -**File**: `templates/stats.templ` (162 lines) - -**Replace script block** (lines 9-11): -```templ - - - -``` - -**Change to**: -```templ - -``` - ---- - -#### 22. templates/sync.templ -**File**: `templates/sync.templ` (184 lines) - -**Replace script block** (lines 9-12): -```templ - - - - -``` - -**Change to**: -```templ - -``` - ---- - -#### 23. templates/testing_templ.templ -**File**: `templates/testing_templ.templ` (115 lines) - -**Replace script block** (lines 9-11): -```templ - - - -``` - -**Change to**: -```templ - -``` - ---- - -#### 24. themes/obsidian.templ -**File**: `themes/obsidian.templ` (91 lines) - -**Replace script block** (lines 9-11): -```templ - - - -``` - -**Change to**: -```templ - -``` - ---- - -#### 25. themes/whatsapp.templ -**File**: `themes/whatsapp.templ` (86 lines) - -**Replace script block** (lines 9-11): -```templ - - - -``` - -**Change to**: -```templ - -``` - ---- - -#### 26. themes/midnight.templ -**File**: `themes/midnight.templ` (91 lines) - -**Replace script block** (lines 9-11): -```templ - - - -``` - -**Change to**: -```templ - -``` - ---- - -#### 27. themes/sunset.templ (same pattern as other themes) -**File**: `themes/sunset.templ` (86 lines) - -**Replace script block** (lines 9-11): -```templ - - - -``` - -**Change to**: -```templ - -``` - ---- - -### Remaining Templates (7-26) - -For templates **7-26**, follow this pattern: - -1. **Remove all individual script tags** (usually 2-5 scripts) -2. **Replace with single script**: `` -3. **Replace `onclick` with `@click`** -4. **Add `x-data` for stateful components** (modals, dropdowns) - -**Quick find-replace patterns**: -```bash -# In each template file: -onclick="funcName()" → @click="module.funcName()" -onclick="func('param')" → @click="module.func('param')" -class="hidden" → x-show="!isOpen" (with x-data parent) -``` - -**Common stateful patterns**: - -**Modal pattern**: -```templ - - - - - -
- -
- -
-
-``` - -**Confirm delete pattern**: -```templ - - - - - -``` - -**Form submission** (keep HTMX for server calls): -```templ - -
- - - Name required -
-``` -**File**: `themes/sunset.templ` (86 lines) - -**Replace script block** (lines 9-11): -```templ - - - -``` - -**Change to**: -```templ - -``` - ---- - -## Phase 4: Build and Verify - -### Step 1: Create Alpine Registration File -**File**: `web/src/alpine.ts` (already exists, needs update) - -**Current file** (missing window.Alpine): -```typescript -import Alpine from "alpinejs"; - -// Initialize Alpine -Alpine.start(); - -// Re-export Alpine for other modules to use -export { Alpine }; -``` - -**Change to** (add window.Alpine + type declaration): -```typescript -import Alpine from "alpinejs"; - -// Extend Window interface to include Alpine -declare global { - interface Window { - Alpine: typeof Alpine; - } -} - -// Initialize Alpine -window.Alpine = Alpine; -Alpine.start(); - -// Re-export Alpine for other modules to use -export { Alpine }; -``` - -**Why type declaration**: Fixes TypeScript error "Property 'Alpine' does not exist on type 'Window'" - -**Why `window.Alpine`**: Required for Alpine DevTools browser extension to work. - -### Step 2: Update main.ts -**File**: `web/src/main.ts` - -**Add at end** (after all other imports): -```typescript -import './alpine'; -``` - -### Step 3: Build the Bundle -```bash -npm run build:ts -``` - -**Expected output**: Creates `web/static/main.js` (~350-450KB unminified, ~120-150KB minified with Alpine bundled) + `main.js.map` sourcemap - -**Why larger**: Alpine.js adds ~15KB gzipped to bundle (worth it for cleaner code). - -### Step 4: Regenerate Templates -```bash -templ generate -``` - -**Expected output**: Regenerates all .templ files with updated Alpine directives - -### Step 5: Verify Alpine is Loaded -```bash -head -n 50 web/static/main.js | grep -i alpine -``` - -**Check for**: Alpine initialization code present in bundle. - -### Step 6: Test in Browser - -1. Start dev server: `go run .` -2. Open browser DevTools (F12) → Console -3. Verify Alpine loaded: - ```javascript - typeof window.Alpine !== 'undefined' // should be true - ``` - -4. Test key pages: - - **Dashboard** (tests theme dropdown, wood paneling) - - **Docs page** (tests lunr search, sidebar toggle) - - **Collections** (tests modal, multiple onclick handlers) - - **Header** (tests theme dropdown, user menu) - - **Devices** (tests multiple modals) - -5. **Check Alpine DevTools** (optional): - - Install Alpine DevTools browser extension - - Inspect `x-data` components in DevTools panel - - Verify reactive state changes - -### Step 7: Verify Functionality - -**In browser console, test Alpine globals**: -```javascript -// Should all be accessible via Alpine -Alpine.stores?.toast // Toast store -Alpine.global('showToast') // Global function -``` - -**Test interactions**: -- Theme dropdown opens/closes smoothly -- Modals open with transitions -- Toast notifications appear -- Forms still submit via HTMX -- Search works on docs page -- All 151 onclick handlers work with `@click` - -### Step 8: Check Bundle Size -```bash -ls -lh web/static/main.js -``` - -**Expected**: ~120-150KB (minified with Alpine included) - -**Breakdown**: -- App code: ~100KB -- Alpine.js: ~15KB gzipped -- Lunr: ~10KB gzipped -- Highlight.js: ~5KB gzipped -- **Total**: ~130KB / ~40KB gzipped - ---- - -## Phase 5: Migrate TypeScript Files to Alpine (Step-by-Step) - -### Priority 1: Core Utilities (Do These First) - -#### 1. web/src/toast.ts -**Current state**: Already has Alpine import at line 1, but not using it yet - -**Add import** (already at line 1): -```typescript -import { Alpine } from "./alpine"; -``` - -**Replace export** (lines 229-236): -```typescript -// Before -(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 -// After - 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), -}); -``` - -**Template usage**: -```html - - - - - -``` - -#### 2. web/src/api.ts -**Add import** (at top): -```typescript -import { Alpine } from './alpine'; -``` - -**Replace export** (lines 90-99): -```typescript -// Before -(window as any).api = { - get: apiGet, - post: apiPost, - put: apiPut, - delete: apiDelete, - patch: apiPatch, - handleResponse, - handleVoidResponse, - handleError, -}; -``` - -**Change to**: -```typescript -// After - Register API with Alpine -Alpine.global('api', { - get: apiGet, - post: apiPost, - put: apiPut, - delete: apiDelete, - patch: apiPatch, - handleResponse, - handleVoidResponse, - handleError, -}); -``` - -**Also update line 85-87** (toast error calls): -```typescript -// Before -if ((window as any).showToast?.error) { - (window as any).showToast.error(message); -} - -// After (will work once toast.ts is migrated) -if (window.Alpine?.stores?.toast?.error) { - window.Alpine.stores.toast.error(message); -} -// Or keep using window.showToast during migration -``` - -**Template usage**: -```html - - - - - -``` - -#### 3. web/src/storage.ts -**Add import** (at top): -```typescript -import { Alpine } from './alpine'; -``` - -**Replace export** (lines 53-67): -```typescript -// Before -(window as any).storage = { - getToken, - setToken, - removeToken, - getRefreshToken, - setRefreshToken, - removeRefreshToken, - getTheme, - setTheme, - getSelectedLibrary, - setSelectedLibrary, - getSelectedBook, - setSelectedBook, - clearAll, -}; -``` - -**Change to**: -```typescript -// After - Register storage helpers with Alpine -Alpine.global('storage', { - getToken, - setToken, - removeToken, - getRefreshToken, - setRefreshToken, - removeRefreshToken, - getTheme, - setTheme, - getSelectedLibrary, - setSelectedLibrary, - getSelectedBook, - setSelectedBook, - clearAll, -}); -``` - -**Template usage**: -```html - - - - - -``` - -### Priority 2: Stateful Components - -#### 4. web/src/header.ts (Theme Dropdown) -**Add import** (at top): -```typescript -import { Alpine } from './alpine'; -import { changeThemeTo, loadTheme, applyTheme } from './theme'; -``` - -**Add Alpine component** (at end of file): -```typescript -Alpine.data('themeDropdown', () => ({ - open: false, - - toggle() { - this.open = !this.open; - }, - - changeTheme(theme: string) { - changeThemeTo(theme); - this.open = false; - }, - - init() { - const savedTheme = loadTheme(); - if (savedTheme) applyTheme(savedTheme); - } -})); -``` - -**Template usage**: -```html -
- -
- -
-
-``` - -#### 5. web/src/woodPaneling.ts -**Add Alpine component** (at end of file): -```typescript -import { Alpine } from './alpine'; - -Alpine.data('woodPaneling', () => ({ - current: localStorage.getItem('woodPaneling') || 'none', - - change(style: string) { - this.current = style; - localStorage.setItem('woodPaneling', style); - // Apply logic here... - } -})); -``` - -**Template usage**: -```html -
- - -
-``` - -### Priority 3: Page-Specific Functions - -#### 6. web/src/collections.ts -**Add import**: -```typescript -import { Alpine } from './alpine'; -``` - -**Register as namespace** (at end): -```typescript -Alpine.global('collections', { - back: backToCollections, - showAddModal: showAddBooksModal, - hideAddModal: hideAddBooksModal, - addSelected: addSelectedBooks, - removeBook: removeBook, - navigate: navigateToCollection -}); -``` - -#### 7. web/src/devices.ts, queue.ts, conflicts.ts -**Follow same pattern as collections.ts** - register as namespace with Alpine.global() - ---- - -## Phase 6: Clean Up (Optional) - -### Remove Individual JS Files -After verifying everything works, remove old individual .js files: - -```bash -rm web/static/api.js -rm web/static/events.js -rm web/static/dom.js -rm web/static/toast.js -rm web/static/storage.js -rm web/static/theme.js -rm web/static/header.js -rm web/static/themeDropdown.js -rm web/static/woodPaneling.js -rm web/static/search.js -rm web/static/docs.js -rm web/static/collections.js -rm web/static/conflicts.js -rm web/static/dashboard.js -rm web/static/admin*.js -rm web/static/analytics.js -rm web/static/bookshelf.js -rm web/static/devices.js -rm web/static/index.js -rm web/static/login.js -rm web/static/profile.js -rm web/static/queue.js -rm web/static/custom_section.js -rm web/static/progress.js -rm web/static/register.js -rm web/static/settings.js -rm web/static/stats.js -rm web/static/sync.js -rm web/static/testing_templ.js -rm web/static/obsidian.js -rm web/static/whatsapp.js -rm web/static/midnight.js -rm web/static/sunset.js -``` - -**Note**: Keep `main.js` and `main.js.map` - ---- - -## Troubleshooting - -### Issue: "Alpine is not defined" -**Cause**: Alpine not initialized or alpine.ts not imported in main.ts -**Fix**: Ensure `import './alpine';` is at end of main.ts and Alpine.start() is called - -### Issue: "Alpine.global is not a function" -**Cause**: Using old Alpine syntax -**Fix**: Use `Alpine.data()` for components or register globals before `Alpine.start()` - -### Issue: "lunr is not defined" -**Cause**: Missing import in docs.ts -**Fix**: Ensure `import * as lunr from 'lunr';` is at line 4 - -### Issue: "hljs is not defined" -**Cause**: Missing import in docs.ts -**Fix**: Ensure `import hljs from 'highlight.js';` is at line 5 - -### Issue: `@click` handlers not working -**Cause**: Alpine not loaded or syntax error in directive -**Fix**: Check browser console, verify Alpine initialized, check directive syntax - -### Issue: `x-show` elements always visible -**Cause**: Missing `x-cloak` CSS or Alpine not loaded before DOM ready -**Fix**: Add `[x-cloak] { display: none !important; }` to CSS, ensure Alpine loads early - -### Issue: Bundle too large (>200KB minified) -**Cause**: Check if large dependencies accidentally bundled -**Fix**: Verify Chart.js is NOT bundled (should remain CDN link in analytics.templ) - -### Issue: 404 errors for .js files -**Cause**: Script tags not updated in templates -**Fix**: Check that all template script tags point to `/static/main.js` - -### Issue: Theme dropdown doesn't close -**Cause**: Missing `@click.away` directive -**Fix**: Add `@click.away="open = false"` to dropdown element - -### Issue: Search not working on docs page -**Cause**: lunr not properly imported or bundled -**Fix**: Check docs.ts imports and rebuild with `npm run build:ts` - -### Issue: Modals don't open -**Cause**: `x-show` variable not reactive or parent `x-data` missing -**Fix**: Ensure modal is wrapped in `x-data="{ modalOpen: false }"` and button uses `@click="modalOpen = true"` - -### Issue: HTMX forms stopped working after Alpine migration -**Cause**: Alpine event handlers conflicting with HTMX -**Fix**: HTMX and Alpine work together - ensure `hx-post` is on form, `@click` is on buttons (not form) - ---- - -## External Dependencies (Not Bundled) - -### Chart.js -**File**: `templates/analytics.templ` (line 14) -**Reason**: 3.4MB minified, only used on 1 page (analytics) -**Approach**: Keep as CDN link: `` - -### htmx.org -**File**: Loaded via separate script tag in base templates -**Reason**: Core framework, needs to load before main.js -**Approach**: Keep as separate script tag (not bundled), works great with Alpine - ---- - -## Architecture Notes - -### Why Alpine.js Over Window Exports? -**Alpine is the modern approach** for this codebase: -1. **Reactive state management**: `x-data`, `x-show`, `x-model` instead of manual DOM manipulation -2. **Clean templates**: `@click` instead of `onclick="window.func()"` -3. **Component-based**: `Alpine.data()` for reusable components -4. **SSR-friendly**: Works with progressive enhancement -5. **No global pollution**: Functions registered in Alpine scope, not window -6. **Better DX**: Alpine DevTools for debugging reactive state - -### Hybrid Approach: Alpine + HTMX -- **Alpine**: Client-side state (modals, dropdowns, theme, form validation) -- **HTMX**: Server calls (form submissions, API calls, data fetching) -- **Why both**: HTMX excels at server communication, Alpine excels at client-side state - -### ESBuild + Alpine Benefits -- **Single bundle**: Alpine bundled with app code (~15KB gzipped) -- **No CDN dependency**: Faster load, no network request for Alpine -- **Tree-shaking**: Unused Alpine features not included -- **ES2020 target**: Works on Chrome 80+, Firefox 72+, Safari 13.1+ - -### Migration Path: Window → Alpine -**Old approach**: -```typescript -(window as any).showToast = showToast; -``` -```html - -``` - -**New approach**: -```typescript -Alpine.global('showToast', showToast); -``` -```html - -``` - -### Why Not Put Everything in main.ts? -Main.ts stays simple (24 lines) because: -- **Imports where used**: docs.ts imports lunr, not main.ts -- **Tree-shaking**: Only what's used gets bundled -- **Maintainability**: Each file has its own dependencies -- **Testing**: Can test individual modules independently -- **Alpine registration**: Each file registers its own Alpine globals/components - ---- - -## Success Criteria - -✅ Single `main.js` bundle (~130KB minified with Alpine) -✅ All 27 templates use single script tag -✅ No 404 errors for missing .js files -✅ Alpine.js loaded and functional -✅ No `(window as any)` usage in TypeScript -✅ Lunr search works on docs page -✅ Syntax highlighting works on docs page -✅ Theme dropdown opens/closes with Alpine -✅ Modals use `x-show`/`x-transition` -✅ All `@click` handlers work (151 migrated) -✅ Toast notifications work via Alpine -✅ HTMX forms still submit correctly -✅ Chart.js loads on analytics page (CDN) -✅ Browser console shows no errors -✅ Alpine DevTools shows reactive components -✅ Bundle targets ES2020 for broad compatibility -✅ Sourcemap generated for debugging - ---- - -## Post-Migration: Development Workflow - -### Watch Mode (Development) -```bash -npm run dev -``` -Watches both TypeScript and templates, rebuilds on changes. - -### Production Build -```bash -npm run build:ts -templ generate -go build -``` -Creates minified bundle, regenerates templates, builds Go binary. - -### Debugging -Use `main.js.map` sourcemap in browser DevTools to debug original TypeScript sources. - ---- - -## Files Modified Summary - -### New Files Created -- `web/src/alpine.ts` (new) - Alpine initialization and exports - -### Configuration Files -- ✅ package.json (already correct - no changes needed) -- ✅ tsconfig.json (already ES2020 - no changes needed) - -### Source Files Modified -- `web/src/main.ts` (add `import './alpine'` at end) -- `web/src/docs.ts` (remove 4 window globals, add Alpine registration) -- `web/src/toast.ts` (replace window export with Alpine.global) -- `web/src/api.ts` (replace window export with Alpine.global) -- `web/src/storage.ts` (replace window export with Alpine.global) -- `web/src/events.ts` (replace window export with Alpine.global) -- `web/src/header.ts` (add Alpine.data themeDropdown component) -- `web/src/woodPaneling.ts` (add Alpine.data component) -- `web/src/collections.ts` (register namespace with Alpine.global) -- `web/src/conflicts.ts` (register namespace with Alpine.global) -- `web/src/devices.ts` (register namespace with Alpine.global) -- `web/src/queue.ts` (register namespace with Alpine.global) -- `web/src/search.ts` (register namespace with Alpine.global) -- `web/src/dom.ts` (register namespace with Alpine.global) -- Plus 10+ more TypeScript files (register functions with Alpine) - -### Template Files Modified (27 files) -All templates updated to: -1. Remove individual script tags -2. Use single `` -3. Replace `onclick` with `@click` -4. Add `x-data` for stateful components (modals, dropdowns) - -Templates: -1. templates/collections.templ (migrate 7 onclick handlers) -2. templates/docs.templ (migrate sidebar toggle to Alpine) -3. templates/admin.templ -4. templates/admin_library.templ -5. templates/dashboard.templ (theme dropdown, wood paneling) -6. templates/header.templ (theme dropdown, user menu - KEY FILE) -7. templates/analytics.templ (keep Chart.js CDN) -8. templates/bookshelf.templ -9. templates/devices.templ (multiple modals) -10. templates/index.templ -11. templates/login.templ -12. templates/profile.templ -13. templates/queue.templ -14. templates/conflicts.templ -15. templates/custom_section.templ -16. templates/admin_users.templ -17. templates/collection_rules.templ -18. templates/progress.templ -19. templates/register.templ -20. templates/settings.templ -21. templates/stats.templ -22. templates/sync.templ -23. templates/testing_templ.templ -24. themes/obsidian.templ -25. themes/whatsapp.templ -26. themes/midnight.templ -27. themes/sunset.templ - -### Generated Files -- `web/static/main.js` (new bundled output with Alpine) -- `web/static/main.js.map` (new sourcemap) - -### Files to Delete (After Verification) -All individual .js files in `web/static/` (except main.js and main.js.map): -- api.js, events.js, dom.js, toast.js, storage.js -- theme.js, header.js, themeDropdown.js, woodPaneling.js -- docs.js, search.js, collections.js, conflicts.js -- dashboard.js, admin*.js, analytics.js, bookshelf.js -- devices.js, index.js, login.js, profile.js, queue.js -- custom_section.js, progress.js, register.js, settings.js -- stats.js, sync.js, testing_templ.js -- obsidian.js, whatsapp.js, midnight.js, sunset.js - ---- - -## Migration Checklist - -Use this checklist to track progress: - -### Phase 1: Clean Up docs.ts -- [ ] Add imports for lunr and hljs (lines 3-4) -- [ ] Remove `(window as any).lunr` check (line 50) -- [ ] Replace `(window as any).lunrIndex` with `lunr.Builder.loadJs` (line 56) -- [ ] Replace `(window as any).docsData` with `docs` import (line 73) -- [ ] Replace window export with Alpine registration (line 99) - -### Phase 2: Create Alpine Infrastructure -- [ ] Create `web/src/alpine.ts` with Alpine initialization -- [ ] Add `import './alpine'` to `web/src/main.ts` -- [ ] Build and verify Alpine is loaded (`npm run build:ts`) - -### Phase 3: Migrate Core TypeScript Files (Priority 1) -- [ ] `web/src/toast.ts` - Register with Alpine.global -- [ ] `web/src/api.ts` - Register with Alpine.global -- [ ] `web/src/storage.ts` - Register with Alpine.global -- [ ] `web/src/events.ts` - Register with Alpine.global -- [ ] `web/src/dom.ts` - Register with Alpine.global - -### Phase 4: Migrate Stateful Components (Priority 2) -- [ ] `web/src/header.ts` - Create themeDropdown Alpine.data component -- [ ] `web/src/woodPaneling.ts` - Create woodPaneling Alpine.data component -- [ ] Test theme dropdown in browser -- [ ] Test wood paneling in browser - -### Phase 5: Migrate Page-Specific Files (Priority 3) -- [ ] `web/src/collections.ts` - Register namespace -- [ ] `web/src/devices.ts` - Register namespace -- [ ] `web/src/queue.ts` - Register namespace -- [ ] `web/src/conflicts.ts` - Register namespace -- [ ] `web/src/search.ts` - Register namespace - -### Phase 6: Update Templates (27 files) -- [ ] templates/header.templ (CRITICAL - theme dropdown) -- [ ] templates/docs.templ (sidebar toggle) -- [ ] templates/dashboard.templ (wood paneling) -- [ ] templates/collections.templ (7 onclick handlers) -- [ ] templates/admin.templ through themes/sunset.templ (23 remaining) -- [ ] Run `templ generate` after template changes - -### Phase 7: Test and Verify -- [ ] Build: `npm run build:ts` -- [ ] Check bundle size: ~130KB minified -- [ ] Start dev server: `go run .` -- [ ] Test all 151 onclick handlers -- [ ] Verify Alpine DevTools shows components -- [ ] Test HTMX forms still work -- [ ] Check browser console for errors -- [ ] Test on Chrome, Firefox, Safari (ES2020 compatibility) - -### Phase 8: Clean Up -- [ ] Delete all individual .js files from web/static/ -- [ ] Commit changes -- [ ] Deploy and test in production - ---- - -## Additional Resources - -### Alpine.js Documentation -- Official docs: https://alpinejs.dev/ -- Essentials guide: https://alpinejs.dev/essentials/start -- `x-data` docs: https://alpinejs.edu/directives/data -- `@click` docs: https://alpinejs.edu/directives/on -- `x-show` docs: https://alpinejs.edu/directives/show -- `x-transition` docs: https://alpinejs.edu/directives/transition - -### Alpine + HTMX Integration -- Blog post: https://htmx.org/examples/blog/ -- Both work together seamlessly - Alpine for client state, HTMX for server calls - -### ESBuild Documentation -- Official docs: https://esbuild.github.io/ -- API: https://esbuild.github.io/api/ -- Bundling: https://esbuild.github.io/api/#bundle - -### Migration Tips -1. **Start small**: Migrate 1-2 files at a time, test frequently -2. **Use Alpine DevTools**: Install browser extension for debugging -3. **Keep HTMX for forms**: Don't replace `hx-post` with Alpine fetch -4. **Test in browser**: Console errors will show missing Alpine globals -5. **Progressive migration**: Can use window exports AND Alpine during transition - -### Configuration -- ✅ package.json (already correct) -- ✅ tsconfig.json (already ES2020) - -### Source Files -- `web/src/docs.ts` (add imports, remove 4 window globals) - -### Template Files (27 files) -All templates updated to use single `` tag: - -1. templates/collections.templ -2. templates/docs.templ -3. templates/admin.templ -4. templates/admin_library.templ -5. templates/dashboard.templ -6. templates/header.templ -7. templates/analytics.templ -8. templates/bookshelf.templ -9. templates/devices.templ -10. templates/index.templ -11. templates/login.templ -12. templates/profile.templ -13. templates/queue.templ -14. templates/conflicts.templ -15. templates/custom_section.templ -16. templates/admin_users.templ -17. templates/collection_rules.templ -18. templates/progress.templ -19. templates/register.templ -20. templates/settings.templ -21. templates/stats.templ -22. templates/sync.templ -23. templates/testing_templ.templ -24. themes/obsidian.templ -25. themes/whatsapp.templ -26. themes/midnight.templ -27. themes/sunset.templ - -### Generated Files -- `web/static/main.js` (new bundled output) -- `web/static/main.js.map` (new sourcemap) - -### Files to Delete (After Verification) -All individual .js files in `web/static/` (except main.js and main.js.map) - ---- - -## Summary: What We're Achieving - -### The Problem -- 151 inline `onclick` handlers using `window.funcName()` -- 20+ TypeScript files exporting to `(window as any)` -- 27 template files with 3-5 script tags each -- Manual DOM manipulation for state (modals, dropdowns) -- No reactive state management -- Global namespace pollution - -### The Solution -1. **Bundle with ESBuild**: Single ~130KB minified bundle (ES2020 target) -2. **Alpine.js for state**: Reactive components with `x-data`, `@click`, `x-show` -3. **Keep HTMX for forms**: Server-side communication remains unchanged -4. **Modern patterns**: No more `(window as any)`, clean templates - -### Benefits -- ✅ **Performance**: Single HTTP request for all JS, better caching -- ✅ **Maintainability**: Alpine components vs imperative DOM manipulation -- ✅ **Type safety**: No more `window as any` hacks -- ✅ **Developer experience**: Clean templates, Alpine DevTools, sourcemaps -- ✅ **User experience**: Smooth transitions, reactive UI, faster loads -- ✅ **Bundle size**: Only ~40KB gzipped (was 30+ HTTP requests before) - -### Key Architecture Decisions -1. **Alpine over vanilla event listeners**: Reactive state is cleaner -2. **Alpine over React/Vue**: Lightweight, SSR-friendly, works with HTMX -3. **Bundle Alpine**: No CDN dependency, faster load, tree-shaking -4. **Keep HTMX**: Don't fix what works - HTMX excels at server communication -5. **Progressive migration**: Migrate incrementally, can mix old/new during transition - -### Migration Effort -- **TypeScript files**: 20+ files (5-10 min each) = ~3 hours -- **Templates**: 27 files (10-15 min each) = ~5 hours -- **Testing**: 2-3 hours -- **Total**: ~10-12 hours for complete migration - -### Maintenance Going Forward -- Adding new feature? Create Alpine component or register function -- New page? Single `") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "Collections - Bookhoard") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -57,7 +57,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("/collections/" + col.ID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 64, Col: 84} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 63, Col: 84} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -70,7 +70,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(col.Color) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 68, Col: 30} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 67, Col: 30} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -83,7 +83,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(col.Icon) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 71, Col: 41} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 70, Col: 41} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -96,7 +96,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs("/collections/" + col.ID + "/edit-modal") if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 74, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 73, Col: 60} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -109,7 +109,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("/api/collections/" + col.ID + "") if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 83, Col: 56} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 82, Col: 56} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -122,7 +122,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(col.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 93, Col: 92} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 92, Col: 92} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -135,7 +135,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(col.Description) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 94, Col: 86} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 93, Col: 86} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { @@ -190,7 +190,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 111, Col: 27} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 110, Col: 27} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { @@ -211,7 +211,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo var templ_7745c5c3_Var11 string templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Icon) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 124, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 123, Col: 81} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { @@ -224,7 +224,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 126, Col: 90} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 125, Col: 90} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { @@ -237,13 +237,13 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo var templ_7745c5c3_Var13 string templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Description) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 127, Col: 71} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 126, Col: 71} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

Books in this Collection

0 selected
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

Books in this Collection

0 selected
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -261,7 +261,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 185, Col: 22} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 184, Col: 22} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { @@ -279,7 +279,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo var templ_7745c5c3_Var15 string templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 192, Col: 27} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 191, Col: 27} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { @@ -302,7 +302,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo var templ_7745c5c3_Var16 string templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(book.CoverImagePath) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 200, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 199, Col: 36} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil {