feat(ui): refresh auth, entry, admin, and secondary pages

Apply the new design-system primitives across the remaining pages so the
whole app shares one visual language:

- Auth/entry: redesigned index, login, register (centered card layout,
  SVG icons, semantic tokens)
- Admin: sidebar nav with icons, cohesive admin dashboard/users/library/
  processing-issues/settings (tables use border-line + hover states;
  scan/websocket attributes preserved)
- Secondary: analytics (stat cards), devices, progress, conflicts, queue,
  profile + form/modal all migrated to .card/.btn/.input primitives and
  SVG action icons
- Docs + API explorer + setup wizard + collection/rules/modals +
  unlinked books + book-detail modals: consistent chrome, modal scrims
  use --surface-overlay, close buttons use icon-btn
- reader.templ and error.templ intentionally left unchanged
This commit is contained in:
2026-08-05 16:01:22 -04:00
parent 28e40e302c
commit a075a5061f
54 changed files with 2740 additions and 2128 deletions
+92 -98
View File
@@ -14,8 +14,8 @@ templ Setup() {
<body class="theme-tokyo-night min-h-screen" x-data="setupWizard" x-init="initSetup()">
<div class="container mx-auto px-4 py-8 max-w-2xl">
<div class="text-center mb-8">
<h1 class="text-4xl font-bold mb-2" style="color: var(--text-primary)">Welcome to Bookhoard</h1>
<p class="text-lg" style="color: var(--text-secondary)">Let's get your library set up.</p>
<h1 class="text-2xl font-bold tracking-tight text-content mb-2">Welcome to Bookhoard</h1>
<p class="text-lg text-content-muted">Let's get your library set up.</p>
</div>
<!-- Step indicator -->
@@ -39,77 +39,72 @@ templ Setup() {
</div>
<!-- Error display -->
<div x-show="errorMsg" class="mb-4 p-4 rounded-lg border border-red-500/50 bg-red-500/10 text-red-400 text-sm">
<div x-show="errorMsg" x-cloak class="mb-4 p-4 rounded-lg border border-red-500/50 bg-red-500/10 text-red-400 text-sm">
<span x-text="errorMsg"></span>
</div>
<!-- ==================== STEP 1: Create Admin ==================== -->
<div x-show="currentStep === 1" class="card p-6 rounded-lg shadow-md border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Create Administrator Account</h2>
<p class="mb-6" style="color: var(--text-secondary)">This first account will have full admin privileges.</p>
<div x-show="currentStep === 1" x-cloak class="card p-6">
<h2 class="text-2xl font-bold tracking-tight text-content mb-2">Create Administrator Account</h2>
<p class="mb-6 text-content-muted">This first account will have full admin privileges.</p>
<form @submit.prevent="submitAdmin()">
<div class="mb-6 p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Server URL (Base URL)</label>
<div class="mb-6 p-4 rounded-lg border border-line bg-surface">
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Server URL (Base URL)</label>
<input
type="url"
x-model="admin.baseUrl"
placeholder="https://books.example.com"
class="w-full px-3 py-2 border rounded mt-2"
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
class="input w-full mt-2"
required
/>
<p class="text-sm mt-2" style="color: var(--text-secondary)">The public URL of your Bookhoard instance. Used for device sync, OPDS feed, and API endpoints.</p>
<p class="text-sm mt-2 text-content-muted">The public URL of your Bookhoard instance. Used for device sync, OPDS feed, and API endpoints.</p>
</div>
<div class="mb-4">
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Email</label>
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Email</label>
<input
type="email"
id="email"
x-model="admin.email"
class="w-full px-3 py-2 border rounded"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
class="input w-full"
required
/>
</div>
<div class="mb-4">
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Username</label>
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Username</label>
<input
type="text"
x-model="admin.username"
id="username"
class="w-full px-3 py-2 border rounded"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
class="input w-full"
required
/>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">First Name</label>
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">First Name</label>
<input
type="text"
x-model="admin.firstName"
placeholder="Optional"
class="w-full px-3 py-2 border rounded"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
class="input w-full"
/>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Last Name</label>
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Last Name</label>
<input
type="text"
x-model="admin.lastName"
placeholder="Optional"
class="w-full px-3 py-2 border rounded"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
class="input w-full"
/>
</div>
</div>
<div id="password-requirements" class="mb-4 p-4 rounded-lg border text-sm" style="background-color: var(--bg-primary); border-color: var(--border)">
<p class="font-semibold mb-2" style="color: var(--text-primary)">Password Requirements:</p>
<ul class="space-y-1" style="color: var(--text-secondary)">
<div id="password-requirements" class="mb-4 p-4 rounded-lg border border-line bg-surface text-sm">
<p class="font-semibold mb-2 text-content">Password Requirements:</p>
<ul class="space-y-1 text-content-muted">
<li id="req-length" class="flex items-center gap-2">
<span class="requirement-icon"></span> At least 8 characters
</li>
@@ -131,31 +126,29 @@ templ Setup() {
</ul>
</div>
<div class="mb-4">
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Password</label>
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Password</label>
<input
type="password"
x-model="admin.password"
id="password"
class="w-full px-3 py-2 border rounded"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
class="input w-full"
required
/>
</div>
<div class="mb-6">
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Confirm Password</label>
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Confirm Password</label>
<input
type="password"
x-model="admin.confirmPassword"
id="confirm-password"
class="w-full px-3 py-2 border rounded"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"
class="input w-full"
required
/>
</div>
<button
type="submit"
id="register-btn"
class="btn-primary w-full py-2 rounded opacity-50 cursor-not-allowed"
class="btn btn-primary w-full opacity-50 cursor-not-allowed"
:disabled="loading"
x-text="loading ? 'Creating...' : 'Create Admin Account'"
></button>
@@ -163,49 +156,47 @@ templ Setup() {
</div>
<!-- ==================== STEP 2: Create Libraries ==================== -->
<div x-show="currentStep === 2" class="card p-6 rounded-lg shadow-md border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Create Libraries</h2>
<p class="mb-6" style="color: var(--text-secondary)">Add one or more media libraries. You can always add more later.</p>
<div x-show="currentStep === 2" x-cloak class="card p-6">
<h2 class="text-2xl font-bold tracking-tight text-content mb-2">Create Libraries</h2>
<p class="mb-6 text-content-muted">Add one or more media libraries. You can always add more later.</p>
<!-- Created libraries list -->
<div x-show="libraries.length > 0" class="mb-6 space-y-2">
<div x-show="libraries.length > 0" x-cloak class="mb-6 space-y-2">
<template x-for="(lib, idx) in libraries" :key="lib.id">
<div class="p-3 rounded-lg border flex items-center justify-between" style="background-color: var(--bg-primary); border-color: var(--border)">
<div class="p-3 rounded-lg border border-line bg-surface flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="text-xl" x-text="lib.type === 'ebooks' ? '📚' : lib.type === 'comics' ? '📖' : '🗾'"></span>
<div>
<p class="font-medium" style="color: var(--text-primary)" x-text="lib.name"></p>
<p class="text-xs capitalize" style="color: var(--text-secondary)" x-text="lib.type"></p>
<p class="font-medium text-content" x-text="lib.name"></p>
<p class="text-xs capitalize text-content-muted" x-text="lib.type"></p>
</div>
</div>
<button
type="button"
@click="removeLibrary(idx)"
class="text-xs text-red-500 hover:underline"
class="text-xs text-danger hover:underline"
>Remove</button>
</div>
</template>
</div>
<!-- Add library form -->
<form @submit.prevent="addLibrary()" class="space-y-4 p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
<form @submit.prevent="addLibrary()" class="space-y-4 p-4 rounded-lg border border-line bg-surface">
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Library Name</label>
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Library Name</label>
<input
type="text"
x-model="newLibrary.name"
placeholder="My Ebook Library"
class="w-full px-3 py-2 border rounded"
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
class="input w-full"
required
/>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Library Type</label>
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Library Type</label>
<select
x-model="newLibrary.type"
class="w-full px-3 py-2 border rounded"
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
class="input w-full"
>
<option value="ebooks">📚 Ebooks</option>
<option value="comics">📖 Comics</option>
@@ -213,54 +204,53 @@ templ Setup() {
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Description (optional)</label>
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Description (optional)</label>
<input
type="text"
x-model="newLibrary.description"
placeholder="Optional"
class="w-full px-3 py-2 border rounded"
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
class="input w-full"
/>
</div>
<button
type="submit"
class="btn-secondary w-full py-2 rounded"
class="btn btn-secondary w-full"
:disabled="loading || !newLibrary.name.trim()"
x-text="loading ? 'Creating...' : '+ Add This Library'"
></button>
</form>
<div class="flex justify-between mt-6">
<button type="button" @click="currentStep = 1" class="btn-secondary px-4 py-2 rounded">Back</button>
<button type="button" @click="currentStep = 3" class="btn-primary px-6 py-2 rounded">
<button type="button" @click="currentStep = 1" class="btn btn-secondary">Back</button>
<button type="button" @click="currentStep = 3" class="btn btn-primary">
<span x-text="libraries.length > 0 ? 'Next: Configure Folders' : 'Skip Continue'"></span>
</button>
</div>
</div>
<!-- ==================== STEP 3: Configure Folders ==================== -->
<div x-show="currentStep === 3" class="card p-6 rounded-lg shadow-md border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Configure Library Folders</h2>
<p class="mb-6" style="color: var(--text-secondary)">Add the filesystem folders where your media files are stored.</p>
<div x-show="currentStep === 3" x-cloak class="card p-6">
<h2 class="text-2xl font-bold tracking-tight text-content mb-2">Configure Library Folders</h2>
<p class="mb-6 text-content-muted">Add the filesystem folders where your media files are stored.</p>
<div x-show="libraries.length === 0" class="text-center py-8" style="color: var(--text-secondary)">
<div x-show="libraries.length === 0" x-cloak class="text-center py-8 text-content-muted">
<p>No libraries created. You can add folders later from the admin panel.</p>
</div>
<div class="space-y-6">
<template x-for="lib in libraries" :key="lib.id">
<div class="p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
<div class="p-4 rounded-lg border border-line bg-surface">
<div class="flex items-center gap-2 mb-3">
<span class="text-xl" x-text="lib.type === 'ebooks' ? '📚' : lib.type === 'comics' ? '📖' : '🗾'"></span>
<h4 class="font-semibold" style="color: var(--text-primary)" x-text="lib.name"></h4>
<h4 class="font-semibold text-content" x-text="lib.name"></h4>
</div>
<!-- Existing folders for this library -->
<div class="space-y-1 mb-3" x-show="lib.folders && lib.folders.length > 0">
<template x-for="(folder, fIdx) in (lib.folders || [])" :key="folder">
<div class="flex items-center justify-between p-2 rounded text-sm" style="background-color: var(--bg-secondary); border-color: var(--border)">
<span style="color: var(--text-primary)" x-text="folder"></span>
<button type="button" @click="removeFolder(lib.id, folder, fIdx)" class="text-xs text-red-500 hover:underline">Remove</button>
<div class="flex items-center justify-between p-2 rounded text-sm border border-line bg-surface-raised">
<span class="text-content" x-text="folder"></span>
<button type="button" @click="removeFolder(lib.id, folder, fIdx)" class="text-xs text-danger hover:underline">Remove</button>
</div>
</template>
</div>
@@ -271,18 +261,17 @@ templ Setup() {
type="text"
:placeholder="'/path/to/' + lib.type"
:id="'folderInput-' + lib.id"
class="flex-1 px-3 py-2 border rounded text-sm"
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)"
class="input flex-1 text-sm"
/>
<button
type="button"
@click="showFolderBrowser('folderInput-' + lib.id)"
class="btn-secondary px-3 py-2 text-xs rounded"
class="btn btn-secondary text-xs"
>Browse</button>
<button
type="button"
@click="addFolderToLibrary(lib)"
class="btn-primary px-3 py-2 text-xs rounded"
class="btn btn-primary text-xs"
>Add</button>
</div>
</div>
@@ -290,37 +279,37 @@ templ Setup() {
</div>
<div class="flex justify-between mt-6">
<button type="button" @click="currentStep = 2" class="btn-secondary px-4 py-2 rounded">Back</button>
<button type="button" @click="currentStep = 4" class="btn-primary px-6 py-2 rounded">Next: Scan</button>
<button type="button" @click="currentStep = 2" class="btn btn-secondary">Back</button>
<button type="button" @click="currentStep = 4" class="btn btn-primary">Next: Scan</button>
</div>
</div>
<!-- ==================== STEP 4: Scan & Finish ==================== -->
<div x-show="currentStep === 4" class="card p-6 rounded-lg shadow-md border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Scan & Finish</h2>
<p class="mb-6" style="color: var(--text-secondary)">Review your configuration and start the initial scan.</p>
<div x-show="currentStep === 4" x-cloak class="card p-6">
<h2 class="text-2xl font-bold tracking-tight text-content mb-2">Scan & Finish</h2>
<p class="mb-6 text-content-muted">Review your configuration and start the initial scan.</p>
<!-- Summary -->
<div class="space-y-3 mb-6">
<div class="p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
<p class="text-sm font-medium mb-1" style="color: var(--text-secondary)">Server URL</p>
<p style="color: var(--text-primary)" x-text="admin.baseUrl"></p>
<div class="p-4 rounded-lg border border-line bg-surface">
<p class="text-sm font-medium mb-1 text-content-muted">Server URL</p>
<p class="text-content" x-text="admin.baseUrl"></p>
</div>
<div class="p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
<p class="text-sm font-medium mb-1" style="color: var(--text-secondary)">Administrator</p>
<p style="color: var(--text-primary)" x-text="admin.username + ' (' + admin.email + ')'"></p>
<div class="p-4 rounded-lg border border-line bg-surface">
<p class="text-sm font-medium mb-1 text-content-muted">Administrator</p>
<p class="text-content" x-text="admin.username + ' (' + admin.email + ')'"></p>
</div>
<div class="p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border)">
<p class="text-sm font-medium mb-2" style="color: var(--text-secondary)">
<div class="p-4 rounded-lg border border-line bg-surface">
<p class="text-sm font-medium mb-2 text-content-muted">
Libraries (<span x-text="libraries.length"></span>)
</p>
<template x-for="lib in libraries" :key="lib.id">
<div class="flex items-start justify-between text-sm py-1">
<div>
<span style="color: var(--text-primary)" x-text="lib.name"></span>
<span class="ml-2 text-xs px-2 py-0.5 rounded" style="background-color: var(--accent); color: var(--bg-primary)" x-text="lib.type"></span>
<span class="text-content" x-text="lib.name"></span>
<span class="ml-2 text-xs px-2 py-0.5 rounded bg-brand text-surface" x-text="lib.type"></span>
</div>
<div class="text-right" style="color: var(--text-secondary)">
<div class="text-right text-content-muted">
<span x-text="(lib.folders || []).length + ' folder(s)'"></span>
</div>
</div>
@@ -329,16 +318,16 @@ templ Setup() {
</div>
<!-- Scan progress -->
<div x-show="scanStarted" class="mb-6 space-y-3">
<h3 class="font-semibold" style="color: var(--text-primary)">Scan Progress</h3>
<div x-show="scanStarted" x-cloak class="mb-6 space-y-3">
<h3 class="font-semibold text-content">Scan Progress</h3>
<div id="library-progress-list" class="space-y-3">
<template x-for="job in scanJobs" :key="job.jobId">
<div class="p-3 rounded border" style="background-color: var(--bg-primary); border-color: var(--border)">
<div class="p-3 rounded border border-line bg-surface">
<div class="flex justify-between items-center mb-2">
<span class="font-medium" style="color: var(--text-primary)" x-text="job.libraryName"></span>
<span class="text-sm" style="color: var(--text-secondary)" x-text="job.statusText"></span>
<span class="font-medium text-content" x-text="job.libraryName"></span>
<span class="text-sm text-content-muted" x-text="job.statusText"></span>
</div>
<div class="w-full rounded-full h-2" style="background-color: var(--bg-secondary)">
<div class="w-full rounded-full h-2 bg-surface-raised">
<div
class="h-2 rounded-full transition-all duration-500"
:style="'width: ' + job.progress + '%; background-color: var(--accent);'"
@@ -347,36 +336,39 @@ templ Setup() {
</div>
</template>
</div>
<div x-show="scanComplete" class="p-4 rounded-lg border border-green-500/50 bg-green-500/10 text-green-400 text-sm">
<div x-show="scanComplete" x-cloak class="p-4 rounded-lg border border-green-500/50 bg-green-500/10 text-green-400 text-sm">
Initial scan complete! Your libraries are ready to use.
</div>
</div>
<div class="flex justify-between mt-6">
<button type="button" @click="currentStep = 3" class="btn-secondary px-4 py-2 rounded" :disabled="loading">Back</button>
<button type="button" @click="currentStep = 3" class="btn btn-secondary" :disabled="loading">Back</button>
<div class="flex gap-3">
<button
x-show="!scanStarted"
x-cloak
type="button"
@click="startScan()"
class="btn-primary px-6 py-2 rounded"
class="btn btn-primary"
:disabled="loading"
>Start Scan</button>
<button
x-show="scanStarted && !scanComplete"
x-cloak
type="button"
@click="finishSetup()"
class="btn-secondary px-6 py-2 rounded"
class="btn btn-secondary"
:disabled="loading"
>Skip to Dashboard</button>
<button
x-show="scanComplete"
x-cloak
type="button"
@click="finishSetup()"
class="btn-primary px-6 py-2 rounded"
class="btn btn-primary"
:disabled="loading"
x-text="loading ? 'Finishing...' : 'Go to Dashboard'"
></button>
@@ -386,11 +378,13 @@ templ Setup() {
</div>
<!-- Folder Browser Modal (reused from admin library) -->
<div id="folder-browser-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7)">
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div id="folder-browser-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: var(--surface-overlay);">
<div class="card p-6 w-full max-w-md mx-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Browse Folders</h2>
<button type="button" @click="hideFolderBrowser()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)"></button>
<h2 class="text-xl font-bold text-content">Browse Folders</h2>
<button type="button" @click="hideFolderBrowser()" class="icon-btn">
@Icon("close", "h-5 w-5")
</button>
</div>
<div id="folder-browser-content">
<!-- Directory listings rendered by Alpine -->