Comprehensive update to esbuild-setup.md with Alpine.js integration guide for replacing (window as any) pattern with modern reactive framework. Key changes: - Add Alpine.js as recommended approach over vanilla event listeners - Include Phase 2: TypeScript Alpine registration patterns - Update Phase 3: Template changes with @click and x-data examples - Add Phase 5: Step-by-step TypeScript migration with exact line numbers - Fix toast.ts, api.ts, storage.ts examples to match actual code structure - Include troubleshooting for Alpine-specific issues - Add migration checklist and quick reference guide Architecture decisions: - Alpine.js for client-side state (modals, dropdowns, theme) - HTMX for server calls (existing pattern, keep unchanged) - Hybrid approach: Alpine reactive components + HTMX forms - Bundle Alpine with ESBuild (~15KB gzipped) Template updates (27 files): - Replace onclick="func()" with @click="func()" - Add x-data for stateful components - Use x-show/x-transition for modals and dropdowns - Keep HTMX form submissions unchanged TypeScript migrations: - Priority 1: Core utilities (toast.ts, api.ts, storage.ts, events.ts, dom.ts) - Priority 2: Stateful components (header.ts themeDropdown, woodPaneling.ts) - Priority 3: Page-specific functions (collections.ts, devices.ts, etc.) - Register functions with Alpine.global() or Alpine.data() Testing and verification: - Alpine DevTools for debugging reactive state - Build step: esbuild --run scripts/build-docs-search.ts - Verify no 404 errors for missing .js files - Test all 151 onclick handlers work with @click Bundle size: ~130KB minified (~40KB gzipped) with Alpine included Browser support: ES2020 (Chrome 80+, Firefox 72+, Safari 13.1+) Document is now 1,812 lines with comprehensive step-by-step instructions for migrating from (window as any) exports to Alpine.js components.
1954 lines
50 KiB
Markdown
1954 lines
50 KiB
Markdown
# 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+).
|
|
|
|
**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)
|
|
|
|
---
|
|
|
|
## Status: What's Already Done ✅
|
|
|
|
### 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
|
|
|
|
### 2. Build Scripts (Already Correct)
|
|
**File**: `package.json`
|
|
**Status**: ✅ Already configured - NO CHANGES NEEDED
|
|
|
|
Lines 8-10:
|
|
```json
|
|
"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\"",
|
|
```
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
## Phase 1: Clean Up docs.ts
|
|
|
|
**File**: `web/src/docs.ts` (99 lines)
|
|
**Status**: Imports already added ✅
|
|
**Lines to modify**: 50, 56, 73 (remove window globals), 99 (replace with Alpine)
|
|
|
|
**Note**: Lines 3-4 already have the correct imports:
|
|
```typescript
|
|
import * as lunr from "lunr";
|
|
import hljs from "highlight.js";
|
|
```
|
|
|
|
### 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;
|
|
}
|
|
```
|
|
|
|
**Change to**:
|
|
```typescript
|
|
// Lunr now bundled via ESBuild
|
|
```
|
|
|
|
### Step 2: Replace (window as any).lunrIndex with direct lunr usage
|
|
**Location**: Line 56
|
|
|
|
**Current** (lines 55-62):
|
|
```typescript
|
|
const idx = (window as any).lunrIndex;
|
|
if (!idx) {
|
|
searchResults.innerHTML =
|
|
'<p class="p-2 text-sm" style="color: var(--text-secondary)">Search index not loaded</p>';
|
|
searchResults.classList.remove("hidden");
|
|
return;
|
|
}
|
|
```
|
|
|
|
**Change to**:
|
|
```typescript
|
|
const idx = lunr.Builder.loadJs(searchIndex);
|
|
if (!idx) {
|
|
searchResults.innerHTML =
|
|
'<p class="p-2 text-sm" style="color: var(--text-secondary)">Search index not loaded</p>';
|
|
searchResults.classList.remove("hidden");
|
|
return;
|
|
}
|
|
```
|
|
|
|
### Step 3: Replace (window as any).docsData with direct import
|
|
**Location**: Line 73
|
|
|
|
**Current** (lines 70-76):
|
|
```typescript
|
|
.map((result: { ref: string }) => {
|
|
const doc = (window as any).docsData?.[result.ref];
|
|
if (!doc) return "";
|
|
```
|
|
|
|
**Change to**:
|
|
```typescript
|
|
.map((result: { ref: string }) => {
|
|
const doc = docs[result.ref];
|
|
if (!doc) return "";
|
|
```
|
|
|
|
### Step 4: Replace window export with Alpine global
|
|
**Location**: Line 99
|
|
|
|
**Current** (lines 98-99):
|
|
```typescript
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
initializeDocsSearch();
|
|
});
|
|
|
|
(window as any).toggleSidebar = toggleSidebar;
|
|
```
|
|
|
|
**Change to**:
|
|
```typescript
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
initializeDocsSearch();
|
|
|
|
// Register with Alpine globally
|
|
if (typeof window.Alpine !== 'undefined') {
|
|
window.Alpine.effect(() => {
|
|
window.Alpine.global('docs', {
|
|
toggleSidebar
|
|
});
|
|
});
|
|
}
|
|
});
|
|
```
|
|
|
|
**Why Alpine.global()**: Makes `toggleSidebar()` available to Alpine templates via `@click="docs.toggleSidebar()"`
|
|
|
|
---
|
|
|
|
## Phase 2: Migrate TypeScript Files to Alpine Registration
|
|
|
|
**Approach**: Replace `(window as any)` exports with Alpine.js global registration
|
|
|
|
### 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
|
|
import Alpine from 'alpinejs';
|
|
|
|
// 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),
|
|
});
|
|
```
|
|
|
|
**Usage in templates**:
|
|
```html
|
|
<!-- Before -->
|
|
<button onclick="showToast.success('Saved!')">Save</button>
|
|
<button onclick="showToast.error('Failed!')">Retry</button>
|
|
|
|
<!-- After -->
|
|
<button @click="showToast.success('Saved!')">Save</button>
|
|
<button @click="showToast.error('Failed!')">Retry</button>
|
|
```
|
|
|
|
#### Pattern: Namespace Registration (Grouping Related Functions)
|
|
|
|
**Example**: `web/src/api.ts`
|
|
|
|
**Current** (lines 90-99):
|
|
```typescript
|
|
(window as any).api = {
|
|
get: apiGet,
|
|
post: apiPost,
|
|
put: apiPut,
|
|
delete: apiDelete,
|
|
patch: apiPatch,
|
|
handleResponse,
|
|
handleVoidResponse,
|
|
handleError,
|
|
};
|
|
```
|
|
|
|
**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
|
|
<!-- Before -->
|
|
<button onclick="api.post('/api/save', data)">Save</button>
|
|
|
|
<!-- After -->
|
|
<button @click="api.post('/api/save', data)">Save</button>
|
|
```
|
|
|
|
#### 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
|
|
<!-- Before -->
|
|
<button onclick="toggleThemeDropdown()">Theme</button>
|
|
<div id="theme-dropdown" class="hidden">
|
|
<button onclick="changeThemeTo('tokyo-night')">Tokyo Night</button>
|
|
</div>
|
|
|
|
<!-- After -->
|
|
<div x-data="themeDropdown">
|
|
<button @click="toggle()">Theme</button>
|
|
<div x-show="open" @click.away="open = false" x-transition>
|
|
<button @click="changeTheme('tokyo-night')">Tokyo Night</button>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
### 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
|
|
|
|
---
|
|
|
|
## Phase 3: Update Templates to Alpine Directives
|
|
|
|
### Template Migration Strategy
|
|
|
|
**151 onclick handlers need migration** across 27 templates. Use this approach:
|
|
|
|
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 `<script>` tags.
|
|
|
|
**Replace final script** (line 269):
|
|
```templ
|
|
<script src="/static/collections.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
**Update onclick handlers**:
|
|
|
|
**Line 63** - Collection card:
|
|
```templ
|
|
<!-- Before -->
|
|
<div onclick="navigateToCollection(this)" data-href={...}>
|
|
|
|
<!-- After -->
|
|
<div @click="collections.navigate(this)" data-href={...} x-data>
|
|
```
|
|
|
|
**Line 119** - Back button:
|
|
```templ
|
|
<!-- Before -->
|
|
<button onclick="backToCollections()" class="btn-secondary">
|
|
|
|
<!-- After -->
|
|
<button @click="collections.back()" class="btn-secondary">
|
|
```
|
|
|
|
**Line 156** - Show add books modal:
|
|
```templ
|
|
<!-- Before -->
|
|
<button onclick="showAddBooksModal()" class="btn-primary">
|
|
|
|
<!-- After -->
|
|
<button @click="collections.showAddModal()" class="btn-primary">
|
|
```
|
|
|
|
**Lines 231, 252** - Hide modal:
|
|
```templ
|
|
<!-- Before -->
|
|
<button onclick="hideAddBooksModal()">
|
|
|
|
<!-- After -->
|
|
<button @click="collections.hideAddModal()">
|
|
```
|
|
|
|
**Line 255** - Add selected books:
|
|
```templ
|
|
<!-- Before -->
|
|
<button type="button" onclick="addSelectedBooks()">
|
|
|
|
<!-- After -->
|
|
<button type="button" @click="collections.addSelected()">
|
|
```
|
|
|
|
---
|
|
|
|
#### 2. templates/docs.templ
|
|
**File**: `templates/docs.templ` (518 lines)
|
|
|
|
**Remove external scripts** (lines 16-18):
|
|
```templ
|
|
<script src="https://cdn.jsdelivr.net/npm/lunr@2.3.9/lunr.min.js"></script>
|
|
<script src="/static/lunr-flex.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
```
|
|
DELETE these lines entirely.
|
|
|
|
**Note**: `lunr-flex.min.js` doesn't exist - this causes 404 errors.
|
|
|
|
**Replace docs.js script** (line 517):
|
|
```templ
|
|
<script src="/static/docs.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
**Update sidebar toggle** (line 23-27):
|
|
```templ
|
|
<!-- Before -->
|
|
<button
|
|
class="lg:hidden fixed top-4 left-4 z-50 bg-background-secondary border border-border rounded p-2"
|
|
onclick="toggleSidebar()"
|
|
aria-label="Toggle menu">
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<!-- After -->
|
|
<div x-data="{ sidebarOpen: false }">
|
|
<button
|
|
class="lg:hidden fixed top-4 left-4 z-50 bg-background-secondary border border-border rounded p-2"
|
|
@click="sidebarOpen = !sidebarOpen"
|
|
aria-label="Toggle menu">
|
|
```
|
|
|
|
**Add Alpine state to sidebar** (line 37):
|
|
```templ
|
|
<!-- Before -->
|
|
<div class="sidebar fixed left-0 top-0 bottom-0 w-72 bg-background-secondary ...">
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<!-- After -->
|
|
<div class="sidebar fixed left-0 top-0 bottom-0 w-72 bg-background-secondary ..."
|
|
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'"
|
|
x-show="sidebarOpen"
|
|
x-transition.opacity>
|
|
```
|
|
|
|
---
|
|
|
|
#### 3. templates/admin.templ
|
|
**File**: `templates/admin.templ` (125 lines)
|
|
|
|
**Replace script block** (lines 9-11):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/admin.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 4. templates/admin_library.templ
|
|
**File**: `templates/admin_library.templ` (163 lines)
|
|
|
|
**Replace script block** (lines 156-160):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/events.js"></script>
|
|
<script src="/static/dom.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/admin_library.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 5. templates/dashboard.templ
|
|
**File**: `templates/dashboard.templ` (310 lines)
|
|
|
|
**Remove first script block** (lines 15-19):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/events.js"></script>
|
|
<script src="/static/dom.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/storage.js"></script>
|
|
```
|
|
DELETE these lines entirely.
|
|
|
|
**Replace dashboard.js script** (line 80):
|
|
```templ
|
|
<script src="/static/dashboard.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 6. templates/header.templ
|
|
**File**: `templates/header.templ` (188 lines)
|
|
|
|
**Replace script block** (lines 184-187):
|
|
```templ
|
|
<script src="/static/storage.js"></script>
|
|
<script src="/static/theme.js"></script>
|
|
<script src="/static/header.js"></script>
|
|
<script src="/static/themeDropdown.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
**Update theme dropdown** (lines 48-93):
|
|
This is a perfect Alpine use case - replace with Alpine component.
|
|
|
|
**Current structure** (simplified):
|
|
```templ
|
|
<button onclick="toggleThemeDropdown()">Theme</button>
|
|
<div id="theme-dropdown" class="hidden">
|
|
<button onclick="changeThemeTo('tokyo-night')">Tokyo Night</button>
|
|
<button onclick="changeThemeTo('dracula')">Dracula</button>
|
|
<!-- ... more themes ... -->
|
|
</div>
|
|
```
|
|
|
|
**Change to Alpine**:
|
|
```templ
|
|
<div x-data="themeDropdown">
|
|
<button @click="toggle()">Theme</button>
|
|
|
|
<div x-show="open"
|
|
@click.away="open = 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"
|
|
class="absolute right-0 mt-2 w-56 rounded-lg shadow-lg z-50"
|
|
:class="open ? '' : 'hidden'">
|
|
|
|
<div class="py-1">
|
|
<button @click="changeTheme('tokyo-night')"
|
|
class="block w-full text-left px-3 py-2 rounded hover:opacity-80">
|
|
Tokyo Night
|
|
</button>
|
|
|
|
<button @click="changeTheme('dracula')"
|
|
class="block w-full text-left px-3 py-2 rounded hover:opacity-80">
|
|
Dracula
|
|
</button>
|
|
|
|
<!-- ... more themes ... -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
**Update user menu** (lines 57-60, 65):
|
|
```templ
|
|
<!-- Before -->
|
|
<button onclick="toggleUserMenu()">
|
|
<div id="user-menu" class="hidden">
|
|
<button onclick="logout()">Logout</button>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<div x-data="{ userMenuOpen: false }">
|
|
<button @click="userMenuOpen = !userMenuOpen">
|
|
|
|
<div x-show="userMenuOpen"
|
|
@click.away="userMenuOpen = false"
|
|
x-transition>
|
|
<button @click="api.post('/api/auth/logout').then(() => window.location.href = '/login')">
|
|
Logout
|
|
</button>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
#### 7. templates/analytics.templ
|
|
**File**: `templates/analytics.templ` (100 lines)
|
|
|
|
**Remove first script block** (lines 10-14):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/events.js"></script>
|
|
<script src="/static/dom.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
```
|
|
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
|
|
<script src="/static/analytics.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 8. templates/bookshelf.templ
|
|
**File**: `templates/bookshelf.templ` (266 lines)
|
|
|
|
**Remove first script block** (lines 10-13):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/events.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/dom.js"></script>
|
|
```
|
|
DELETE these lines entirely.
|
|
|
|
**Replace bookshelf.js script** (line 63):
|
|
```templ
|
|
<script src="/static/bookshelf.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 9. templates/devices.templ
|
|
**File**: `templates/devices.templ` (870 lines)
|
|
|
|
**Replace script block** (lines 12-16):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/events.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/dom.js"></script>
|
|
<script src="/static/devices.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 10. templates/index.templ
|
|
**File**: `templates/index.templ` (165 lines)
|
|
|
|
**Remove first script block** (lines 10-12):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/events.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
```
|
|
DELETE these lines entirely.
|
|
|
|
**Replace index.js script** (line 138):
|
|
```templ
|
|
<script src="/static/index.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 11. templates/login.templ
|
|
**File**: `templates/login.templ` (81 lines)
|
|
|
|
**Replace script block** (lines 10-12):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/login.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 12. templates/profile.templ
|
|
**File**: `templates/profile.templ` (68 lines)
|
|
|
|
**Replace script block** (lines 9-11):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/profile.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 13. templates/queue.templ
|
|
**File**: `templates/queue.templ` (169 lines)
|
|
|
|
**Remove first script block** (lines 12-15):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/events.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/dom.js"></script>
|
|
```
|
|
DELETE these lines entirely.
|
|
|
|
**Replace queue.js script** (line 166):
|
|
```templ
|
|
<script src="/static/queue.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 14. templates/conflicts.templ
|
|
**File**: `templates/conflicts.templ` (239 lines)
|
|
|
|
**Remove first script block** (lines 12-15):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/events.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/dom.js"></script>
|
|
```
|
|
DELETE these lines entirely.
|
|
|
|
**Replace conflicts.js script** (line 234):
|
|
```templ
|
|
<script src="/static/conflicts.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 15. templates/custom_section.templ
|
|
**File**: `templates/custom_section.templ` (168 lines)
|
|
|
|
**Replace script block** (lines 10-13):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/events.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/custom_section.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 16. templates/admin_users.templ
|
|
**File**: `templates/admin_users.templ` (145 lines)
|
|
|
|
**Replace script block** (lines 9-12):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/events.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/admin_users.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 17. templates/collection_rules.templ
|
|
**File**: `templates/collection_rules.templ` (420 lines)
|
|
|
|
**Replace script block** (lines 10-12):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/events.js"></script>
|
|
<script src="/static/collection_rules.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 18. templates/progress.templ
|
|
**File**: `templates/progress.templ` (114 lines)
|
|
|
|
**Replace script block** (lines 12-14):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/events.js"></script>
|
|
<script src="/static/progress.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 19. templates/register.templ
|
|
**File**: `templates/register.templ` (97 lines)
|
|
|
|
**Replace script block** (lines 10-12):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/register.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 20. templates/settings.templ
|
|
**File**: `templates/settings.templ` (157 lines)
|
|
|
|
**Replace script block** (lines 9-11):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/settings.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 21. templates/stats.templ
|
|
**File**: `templates/stats.templ` (162 lines)
|
|
|
|
**Replace script block** (lines 9-11):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/events.js"></script>
|
|
<script src="/static/stats.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 22. templates/sync.templ
|
|
**File**: `templates/sync.templ` (184 lines)
|
|
|
|
**Replace script block** (lines 9-12):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/events.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/sync.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 23. templates/testing_templ.templ
|
|
**File**: `templates/testing_templ.templ` (115 lines)
|
|
|
|
**Replace script block** (lines 9-11):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/events.js"></script>
|
|
<script src="/static/testing_templ.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 24. themes/obsidian.templ
|
|
**File**: `themes/obsidian.templ` (91 lines)
|
|
|
|
**Replace script block** (lines 9-11):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/obsidian.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 25. themes/whatsapp.templ
|
|
**File**: `themes/whatsapp.templ` (86 lines)
|
|
|
|
**Replace script block** (lines 9-11):
|
|
```templ
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/whatsapp.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 26. themes/midnight.templ
|
|
**File**: `themes/midnight.templ` (91 lines)
|
|
|
|
**Replace script block** (lines 9-11):
|
|
```templ
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/midnight.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
#### 27. themes/sunset.templ (same pattern as other themes)
|
|
**File**: `themes/sunset.templ` (86 lines)
|
|
|
|
**Replace script block** (lines 9-11):
|
|
```templ
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/sunset.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
### 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**: `<script src="/static/main.js"></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
|
|
<!-- Before -->
|
|
<button onclick="showModal()">Open</button>
|
|
<div id="modal" class="hidden">
|
|
<button onclick="hideModal()">Close</button>
|
|
</div>
|
|
|
|
<!-- After -->
|
|
<div x-data="{ modalOpen: false }">
|
|
<button @click="modalOpen = true">Open</button>
|
|
<div x-show="modalOpen" x-transition>
|
|
<button @click="modalOpen = false">Close</button>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
**Confirm delete pattern**:
|
|
```templ
|
|
<!-- Before -->
|
|
<button onclick="confirmDelete()">Delete</button>
|
|
|
|
<!-- After -->
|
|
<button @click="if(confirm('Are you sure?')) module.deleteItem()">Delete</button>
|
|
```
|
|
|
|
**Form submission** (keep HTMX for server calls):
|
|
```templ
|
|
<!-- Keep HTMX for forms -->
|
|
<form hx-post="/api/save" hx-target="#result">
|
|
<!-- Alpine for client-side validation -->
|
|
<input x-model="formData.name" @input="validate = true">
|
|
<span x-show="validate && !formData.name" class="error">Name required</span>
|
|
</form>
|
|
```
|
|
**File**: `themes/sunset.templ` (86 lines)
|
|
|
|
**Replace script block** (lines 9-11):
|
|
```templ
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/sunset.js"></script>
|
|
```
|
|
|
|
**Change to**:
|
|
```templ
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
---
|
|
|
|
## 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
|
|
<!-- Before -->
|
|
<button onclick="showToast.success('Saved!')">Save</button>
|
|
|
|
<!-- After -->
|
|
<button @click="showToast.success('Saved!')">Save</button>
|
|
```
|
|
|
|
#### 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
|
|
<!-- Before -->
|
|
<button onclick="api.post('/api/save', data)">Save</button>
|
|
|
|
<!-- After -->
|
|
<button @click="api.post('/api/save', data)">Save</button>
|
|
```
|
|
|
|
#### 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
|
|
<!-- Before -->
|
|
<button onclick="const token = storage.getToken()">Get Token</button>
|
|
|
|
<!-- After -->
|
|
<button @click="const token = storage.getToken()">Get Token</button>
|
|
```
|
|
|
|
### 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
|
|
<div x-data="themeDropdown">
|
|
<button @click="toggle()">Theme</button>
|
|
<div x-show="open" @click.away="open = false" x-transition>
|
|
<button @click="changeTheme('tokyo-night')">Tokyo Night</button>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
#### 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
|
|
<div x-data="woodPaneling">
|
|
<button @click="change('none')" :class="current === 'none' ? 'active' : ''">None</button>
|
|
<button @click="change('wood-light')" :class="current === 'wood-light' ? 'active' : ''">Wood Light</button>
|
|
</div>
|
|
```
|
|
|
|
### 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: `<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>`
|
|
|
|
### 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
|
|
<button onclick="showToast('Saved!')">Save</button>
|
|
```
|
|
|
|
**New approach**:
|
|
```typescript
|
|
Alpine.global('showToast', showToast);
|
|
```
|
|
```html
|
|
<button @click="showToast('Saved!')">Save</button>
|
|
```
|
|
|
|
### 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 `<script src="/static/main.js"></script>`
|
|
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 `<script src="/static/main.js"></script>` 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 `<script src="/static/main.js">` tag
|
|
- Need state? `x-data="{ open: false }"`
|
|
- Server call? `hx-post="/api/endpoint"` (HTMX)
|
|
|
|
---
|
|
|
|
## Quick Reference: Alpine Syntax
|
|
|
|
| Old (Window) | New (Alpine) |
|
|
|--------------|--------------|
|
|
| `onclick="func()"` | `@click="func()"` |
|
|
| `onclick="obj.method()"` | `@click="obj.method()"` |
|
|
| `class="hidden"` | `x-show="!open"` |
|
|
| Manual DOM toggle | `x-data="{ open: false }"` + `@click="open = !open"` |
|
|
| `onclick="window.api.get()"` | `@click="api.get()"` |
|
|
| Form validation | `x-model="formData.name"` + `<span x-show="!name">Error</span>` |
|
|
|
|
---
|
|
|
|
**Last Updated**: March 2026
|
|
**ESBuild Version**: Latest (via npm)
|
|
**Alpine.js Version**: Latest (via npm)
|
|
**Target**: ES2020 (Chrome 80+, Firefox 72+, Safari 13.1+)
|