CSP: replace 'unsafe-inline' with a content hash for the theme flash guard
script-src now allow-lists the guard by sha256 of its rendered bytes (middleware.ScriptHashThemeFlashGuard) instead of 'unsafe-inline' — the guard is the app's only inline <script>. security_headers_test.go renders templates.ThemeFlashGuard() and fails with the replacement hash if the script text ever drifts. Executing anti-XSS strict CSP surfaced three latent violations the eval-removal sweep couldn't see (hash mode is stricter than eval mode, because inline event handler ATTRIBUTES now count against script-src): 1. admin_archived / admin_library: 15 native onclick=/onsubmit= handlers (modal open/close, folder-browser picker, purge button) converted to Alpine @click/@submit component methods — openCreateLibraryModal, hideModal, pickBrowsePath added to the admin component; purgeArchivedItems promoted from its window global into the component and the (window as any) exposure removed. 2. dashboard/docs: onclick handlers converted — dashboard's event.stopPropagation() → @click.stop; docs' sidebar/search closers → closeDocsSidebar/closeSearchResults component methods. 3. 8 onerror= image-fallback attributes (cover placeholders app-wide + audiobook cover hide) replaced by data-fallback attributes handled by one delegated capture-phase listener in web/src/image_fallback.ts — works for htmx-swapped content without re-binding. Verified: theme guard applies a saved theme on public pages under the hash CSP; 14-page authenticated sweep clean; admin pages clean under the promoted admin; admin-library modal open/close bindings exercised; readers render error-free; go build/vet/middleware-test, tsc, vp check/test (146), build:ts all green.
This commit is contained in:
1 parent
6a598786e9
commit
fb95b6d400
26 files changed
+180
-54
No files matched your search
@@ -36,7 +36,7 @@ templ AdminArchived(user User, items []ArchivedItem, retentionDays int) {
|
||||
</p>
|
||||
</div>
|
||||
if len(items) > 0 {
|
||||
<button type="button" onclick="purgeArchivedItems()" class="btn btn-secondary">
|
||||
<button type="button" @click="purgeArchivedItems()" class="btn btn-secondary">
|
||||
@Icon("trash", "h-4 w-4")
|
||||
Purge All Archived
|
||||
</button>
|
||||
|
||||
@@ -100,7 +100,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(items) > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<button type=\"button\" onclick=\"purgeArchivedItems()\" class=\"btn btn-secondary\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<button type=\"button\" @click=\"purgeArchivedItems()\" class=\"btn btn-secondary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Manage media libraries, folders, and scanning</p>
|
||||
</div>
|
||||
<button
|
||||
onclick="document.getElementById('create-library-modal').classList.remove('hidden')"
|
||||
@click="openCreateLibraryModal()"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
@Icon("plus", "h-4 w-4")
|
||||
@@ -52,7 +52,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="/admin/archived" class="btn btn-secondary">View Archived</a>
|
||||
<button type="button" onclick="purgeArchivedItems()" class="btn btn-secondary">
|
||||
<button type="button" @click="purgeArchivedItems()" class="btn btn-secondary">
|
||||
@Icon("trash", "h-4 w-4")
|
||||
Purge Archived Now
|
||||
</button>
|
||||
@@ -69,7 +69,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
|
||||
<div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Create Library</h2>
|
||||
<button type="button" onclick="document.getElementById('create-library-modal').classList.add('hidden')" class="icon-btn" aria-label="Close">
|
||||
<button type="button" @click="hideModal('create-library-modal')" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
@@ -77,7 +77,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
|
||||
hx-post="/admin/library/create"
|
||||
hx-target="#libraries-container"
|
||||
hx-swap="innerHTML"
|
||||
onsubmit="document.getElementById('create-library-modal').classList.add('hidden')"
|
||||
@submit="hideModal('create-library-modal')"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Library Name</label>
|
||||
@@ -94,7 +94,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" onclick="document.getElementById('create-library-modal').classList.add('hidden')" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" @click="hideModal('create-library-modal')" class="btn btn-secondary">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
@Icon("plus", "h-4 w-4")
|
||||
Create
|
||||
@@ -108,11 +108,11 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
|
||||
<div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Edit Library</h2>
|
||||
<button type="button" onclick="document.getElementById('edit-library-modal').classList.add('hidden')" class="icon-btn" aria-label="Close">
|
||||
<button type="button" @click="hideModal('edit-library-modal')" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<form id="edit-library-form" hx-target="#libraries-container" hx-swap="innerHTML" onsubmit="document.getElementById('edit-library-modal').classList.add('hidden')">
|
||||
<form id="edit-library-form" hx-target="#libraries-container" hx-swap="innerHTML" @submit="hideModal('edit-library-modal')">
|
||||
<div class="mb-4">
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Library Name</label>
|
||||
<input type="text" name="name" id="edit-library-name" class="input" required/>
|
||||
@@ -122,7 +122,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
|
||||
<textarea name="description" id="edit-library-desc" rows="3" class="input"></textarea>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" onclick="document.getElementById('edit-library-modal').classList.add('hidden')" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" @click="hideModal('edit-library-modal')" class="btn btn-secondary">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
@Icon("save", "h-4 w-4")
|
||||
Save
|
||||
@@ -136,7 +136,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
|
||||
<div class="card p-6 w-full max-w-lg mx-4" style="box-shadow: var(--shadow-pop);">
|
||||
<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" onclick="document.getElementById('folder-browser-modal').classList.add('hidden')" class="icon-btn" aria-label="Close">
|
||||
<button type="button" @click="hideModal('folder-browser-modal')" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
@@ -148,7 +148,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
|
||||
<div class="card p-6 w-full max-w-md mx-4" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Delete Library</h2>
|
||||
<button type="button" onclick="document.getElementById('delete-library-modal').classList.add('hidden')" class="icon-btn" aria-label="Close">
|
||||
<button type="button" @click="hideModal('delete-library-modal')" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
@@ -165,7 +165,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
|
||||
</li>
|
||||
</ul>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" onclick="document.getElementById('delete-library-modal').classList.add('hidden')" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" @click="hideModal('delete-library-modal')" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" id="delete-library-confirm" class="btn btn-danger">
|
||||
@Icon("trash", "h-4 w-4")
|
||||
Delete
|
||||
@@ -195,7 +195,7 @@ templ LibraryList(user User, libraries []LibraryData, users []User) {
|
||||
</span>
|
||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Libraries Yet</h3>
|
||||
<p class="mb-4 text-sm" style="color: var(--text-secondary)">Create your first library to get started</p>
|
||||
<button onclick="document.getElementById('create-library-modal').classList.remove('hidden')" class="btn btn-primary">Create Your First Library</button>
|
||||
<button @click="openCreateLibraryModal()" class="btn btn-primary">Create Your First Library</button>
|
||||
</div>
|
||||
} else {
|
||||
<div class="space-y-4">
|
||||
@@ -411,7 +411,7 @@ templ FolderBrowserContent(currentPath string, parentPath string, entries []DirE
|
||||
type="button"
|
||||
data-target-input={ targetInput }
|
||||
data-current-path={ currentPath }
|
||||
onclick="document.getElementById(this.dataset.targetInput).value = this.dataset.currentPath; document.getElementById('folder-browser-modal').classList.add('hidden')"
|
||||
@click="pickBrowsePath($event)"
|
||||
class="btn btn-primary text-sm"
|
||||
>
|
||||
@Icon("check", "h-4 w-4")
|
||||
|
||||
@@ -69,7 +69,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Libraries</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Manage media libraries, folders, and scanning</p></div><button onclick=\"document.getElementById('create-library-modal').classList.remove('hidden')\" class=\"btn btn-primary\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Libraries</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Manage media libraries, folders, and scanning</p></div><button @click=\"openCreateLibraryModal()\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -95,7 +95,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</a></p><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Files missing from disk for two consecutive scans. Reading history is kept unless purged; items return automatically if their files come back.</p></div><div class=\"flex items-center gap-2\"><a href=\"/admin/archived\" class=\"btn btn-secondary\">View Archived</a> <button type=\"button\" onclick=\"purgeArchivedItems()\" class=\"btn btn-secondary\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</a></p><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Files missing from disk for two consecutive scans. Reading history is kept unless purged; items return automatically if their files come back.</p></div><div class=\"flex items-center gap-2\"><a href=\"/admin/archived\" class=\"btn btn-secondary\">View Archived</a> <button type=\"button\" @click=\"purgeArchivedItems()\" class=\"btn btn-secondary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -116,7 +116,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></div></main><!-- Create / Edit Library Modal --><div id=\"create-library-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-md\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Create Library</h2><button type=\"button\" onclick=\"document.getElementById('create-library-modal').classList.add('hidden')\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></div></main><!-- Create / Edit Library Modal --><div id=\"create-library-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-md\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Create Library</h2><button type=\"button\" @click=\"hideModal('create-library-modal')\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -124,7 +124,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</button></div><form hx-post=\"/admin/library/create\" hx-target=\"#libraries-container\" hx-swap=\"innerHTML\" onsubmit=\"document.getElementById('create-library-modal').classList.add('hidden')\"><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Library Name</label> <input type=\"text\" name=\"name\" placeholder=\"My Ebook Library\" class=\"input\" required></div><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea name=\"description\" placeholder=\"Optional description\" rows=\"3\" class=\"input\"></textarea></div><div class=\"mb-6\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Library Type</label> <select name=\"type\" class=\"input\" required>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</button></div><form hx-post=\"/admin/library/create\" hx-target=\"#libraries-container\" hx-swap=\"innerHTML\" @submit=\"hideModal('create-library-modal')\"><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Library Name</label> <input type=\"text\" name=\"name\" placeholder=\"My Ebook Library\" class=\"input\" required></div><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea name=\"description\" placeholder=\"Optional description\" rows=\"3\" class=\"input\"></textarea></div><div class=\"mb-6\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Library Type</label> <select name=\"type\" class=\"input\" required>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -132,7 +132,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</select></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" onclick=\"document.getElementById('create-library-modal').classList.add('hidden')\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</select></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" @click=\"hideModal('create-library-modal')\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -140,7 +140,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "Create</button></div></form></div></div><!-- Edit Library Modal --><div id=\"edit-library-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-md\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Edit Library</h2><button type=\"button\" onclick=\"document.getElementById('edit-library-modal').classList.add('hidden')\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "Create</button></div></form></div></div><!-- Edit Library Modal --><div id=\"edit-library-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-md\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Edit Library</h2><button type=\"button\" @click=\"hideModal('edit-library-modal')\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -148,7 +148,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</button></div><form id=\"edit-library-form\" hx-target=\"#libraries-container\" hx-swap=\"innerHTML\" onsubmit=\"document.getElementById('edit-library-modal').classList.add('hidden')\"><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Library Name</label> <input type=\"text\" name=\"name\" id=\"edit-library-name\" class=\"input\" required></div><div class=\"mb-6\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea name=\"description\" id=\"edit-library-desc\" rows=\"3\" class=\"input\"></textarea></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" onclick=\"document.getElementById('edit-library-modal').classList.add('hidden')\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</button></div><form id=\"edit-library-form\" hx-target=\"#libraries-container\" hx-swap=\"innerHTML\" @submit=\"hideModal('edit-library-modal')\"><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Library Name</label> <input type=\"text\" name=\"name\" id=\"edit-library-name\" class=\"input\" required></div><div class=\"mb-6\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea name=\"description\" id=\"edit-library-desc\" rows=\"3\" class=\"input\"></textarea></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" @click=\"hideModal('edit-library-modal')\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "Save</button></div></form></div></div><!-- Folder Browser Modal --><div id=\"folder-browser-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-lg mx-4\" style=\"box-shadow: var(--shadow-pop);\"><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\" onclick=\"document.getElementById('folder-browser-modal').classList.add('hidden')\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "Save</button></div></form></div></div><!-- Folder Browser Modal --><div id=\"folder-browser-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-lg mx-4\" style=\"box-shadow: var(--shadow-pop);\"><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=\"hideModal('folder-browser-modal')\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -164,7 +164,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</button></div><div id=\"folder-browser-content\"></div></div></div><!-- Delete Library Modal --><div id=\"delete-library-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-md mx-4\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-4\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Delete Library</h2><button type=\"button\" onclick=\"document.getElementById('delete-library-modal').classList.add('hidden')\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</button></div><div id=\"folder-browser-content\"></div></div></div><!-- Delete Library Modal --><div id=\"delete-library-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center p-4\" style=\"background-color: var(--surface-overlay);\"><div class=\"card p-6 w-full max-w-md mx-4\" style=\"box-shadow: var(--shadow-pop);\"><div class=\"flex justify-between items-center mb-4\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Delete Library</h2><button type=\"button\" @click=\"hideModal('delete-library-modal')\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -172,7 +172,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</button></div><p class=\"mb-3\" style=\"color: var(--text-primary)\">Are you sure you want to delete <strong id=\"delete-library-name\"></strong>?</p><ul class=\"mb-6 space-y-2 text-sm list-disc list-inside\" style=\"color: var(--text-primary)\"><li><strong>Permanently removed, no archive or undo:</strong> the library, its folder mappings, and all book records — with their reading progress, highlights, notes, bookmarks, and ratings.</li><li><strong>Not touched:</strong> media files on disk.</li></ul><div class=\"flex justify-end space-x-3\"><button type=\"button\" onclick=\"document.getElementById('delete-library-modal').classList.add('hidden')\" class=\"btn btn-secondary\">Cancel</button> <button type=\"button\" id=\"delete-library-confirm\" class=\"btn btn-danger\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</button></div><p class=\"mb-3\" style=\"color: var(--text-primary)\">Are you sure you want to delete <strong id=\"delete-library-name\"></strong>?</p><ul class=\"mb-6 space-y-2 text-sm list-disc list-inside\" style=\"color: var(--text-primary)\"><li><strong>Permanently removed, no archive or undo:</strong> the library, its folder mappings, and all book records — with their reading progress, highlights, notes, bookmarks, and ratings.</li><li><strong>Not touched:</strong> media files on disk.</li></ul><div class=\"flex justify-end space-x-3\"><button type=\"button\" @click=\"hideModal('delete-library-modal')\" class=\"btn btn-secondary\">Cancel</button> <button type=\"button\" id=\"delete-library-confirm\" class=\"btn btn-danger\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -278,7 +278,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</span><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Libraries Yet</h3><p class=\"mb-4 text-sm\" style=\"color: var(--text-secondary)\">Create your first library to get started</p><button onclick=\"document.getElementById('create-library-modal').classList.remove('hidden')\" class=\"btn btn-primary\">Create Your First Library</button></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</span><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Libraries Yet</h3><p class=\"mb-4 text-sm\" style=\"color: var(--text-secondary)\">Create your first library to get started</p><button @click=\"openCreateLibraryModal()\" class=\"btn btn-primary\">Create Your First Library</button></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -1038,7 +1038,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "\" onclick=\"document.getElementById(this.dataset.targetInput).value = this.dataset.currentPath; document.getElementById('folder-browser-modal').classList.add('hidden')\" class=\"btn btn-primary text-sm\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "\" @click=\"pickBrowsePath($event)\" class=\"btn btn-primary text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
src={ metadata.CoverImagePath }
|
||||
alt={ metadata.Title }
|
||||
class="w-full h-full object-cover"
|
||||
onerror="this.style.display='none'"
|
||||
data-fallback="hide"
|
||||
/>
|
||||
} else {
|
||||
<span class="text-5xl flex items-center justify-center h-full" style="color: var(--text-secondary);">🎧</span>
|
||||
|
||||
@@ -220,7 +220,7 @@ func AudioReader(user User, metadata ReaderMetadata) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" class=\"w-full h-full object-cover\" onerror=\"this.style.display='none'\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" class=\"w-full h-full object-cover\" data-fallback=\"hide\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string, tomb
|
||||
alt={ book.Title }
|
||||
class="w-48 md:w-56 h-72 md:h-80 object-cover rounded-2xl"
|
||||
style="box-shadow: var(--shadow-pop);"
|
||||
onerror="this.src='/static/placeholder-book.svg'"
|
||||
|
||||
/>
|
||||
} else {
|
||||
<img
|
||||
|
||||
@@ -244,7 +244,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string, tombs
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" class=\"w-48 md:w-56 h-72 md:h-80 object-cover rounded-2xl\" style=\"box-shadow: var(--shadow-pop);\" onerror=\"this.src='/static/placeholder-book.svg'\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" class=\"w-48 md:w-56 h-72 md:h-80 object-cover rounded-2xl\" style=\"box-shadow: var(--shadow-pop);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ templ CollectionBooksGrid(collection CollectionData, books []handlers.BookInfo)
|
||||
src={ book.CoverImagePath }
|
||||
alt="Cover"
|
||||
class="w-full aspect-[3/4] object-cover rounded-lg shadow-md"
|
||||
onerror="this.src='/static/placeholder-book.svg'"
|
||||
|
||||
/>
|
||||
} else {
|
||||
<img
|
||||
@@ -452,7 +452,7 @@ templ BookPickerGrid(books []handlers.BookInfo) {
|
||||
>
|
||||
<div class="relative">
|
||||
if book.CoverImagePath != "" {
|
||||
<img src={ book.CoverImagePath } alt={ book.Title } class="w-full aspect-[2/3] object-cover" loading="lazy" onerror="this.src='/static/placeholder-book.svg'"/>
|
||||
<img src={ book.CoverImagePath } alt={ book.Title } class="w-full aspect-[2/3] object-cover" loading="lazy" />
|
||||
} else {
|
||||
<img src="/static/placeholder-book.svg" alt={ book.Title } class="w-full aspect-[2/3] object-cover" loading="lazy"/>
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ func CollectionBooksGrid(collection CollectionData, books []handlers.BookInfo) t
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" alt=\"Cover\" class=\"w-full aspect-[3/4] object-cover rounded-lg shadow-md\" onerror=\"this.src='/static/placeholder-book.svg'\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" alt=\"Cover\" class=\"w-full aspect-[3/4] object-cover rounded-lg shadow-md\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -970,7 +970,7 @@ func BookPickerGrid(books []handlers.BookInfo) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "\" class=\"w-full aspect-[2/3] object-cover\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "\" class=\"w-full aspect-[2/3] object-cover\" loading=\"lazy\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ templ BookCard(item handlers.BookInfo) {
|
||||
alt={ item.Title }
|
||||
class="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
onerror="this.src='/static/placeholder-book.svg'"
|
||||
|
||||
/>
|
||||
} else {
|
||||
<img
|
||||
@@ -347,7 +347,7 @@ templ DashboardWeekWidget(week WeekStatsData, activeLibType string) {
|
||||
<span class="text-sm font-medium" style="color: var(--accent)">🔥 { fmt.Sprintf("%d", week.CurrentStreak) }-day streak</span>
|
||||
}
|
||||
<span class="ml-auto flex items-center gap-3">
|
||||
<a href="/stats" class="text-xs font-medium hover:underline" style="color: var(--accent);" onclick="event.stopPropagation()">Full stats</a>
|
||||
<a href="/stats" class="text-xs font-medium hover:underline" style="color: var(--accent);" @click.stop>Full stats</a>
|
||||
@Icon("chevron-down", "h-4 w-4 transition-transform group-open:rotate-180")
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
@@ -456,7 +456,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" class=\"w-full h-full object-cover\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" class=\"w-full h-full object-cover\" loading=\"lazy\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -1027,7 +1027,7 @@ func DashboardWeekWidget(week WeekStatsData, activeLibType string) templ.Compone
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "<span class=\"ml-auto flex items-center gap-3\"><a href=\"/stats\" class=\"text-xs font-medium hover:underline\" style=\"color: var(--accent);\" onclick=\"event.stopPropagation()\">Full stats</a>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "<span class=\"ml-auto flex items-center gap-3\"><a href=\"/stats\" class=\"text-xs font-medium hover:underline\" style=\"color: var(--accent);\" @click.stop>Full stats</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
||||
<body x-data="docs" class={ "theme-" + user.Theme + " page-docs font-sans antialiased" }>
|
||||
@DocsTopbar(user)
|
||||
<!-- Mobile drawer backdrop -->
|
||||
<div id="docs-overlay" class="hidden fixed inset-0 z-40 lg:hidden" style="background: var(--surface-overlay);" onclick="document.getElementById('docs-sidebar').classList.add('-translate-x-full'); this.classList.add('hidden')"></div>
|
||||
<div id="docs-overlay" class="hidden fixed inset-0 z-40 lg:hidden" style="background: var(--surface-overlay);" @click="closeDocsSidebar($event)"></div>
|
||||
<div class="flex">
|
||||
@DocsSidebar(nav, currentPath)
|
||||
<main class="flex-1 min-w-0">
|
||||
@@ -243,7 +243,7 @@ templ DocsSearchResults(results []SearchResultItem) {
|
||||
<ul class="py-1 list-none m-0 p-0">
|
||||
for _, r := range results {
|
||||
<li>
|
||||
<a href={ r.URL } class="block px-4 py-2 no-underline transition-colors hover:bg-surface-hover" onclick="this.closest('#docs-search-results').classList.add('hidden')">
|
||||
<a href={ r.URL } class="block px-4 py-2 no-underline transition-colors hover:bg-surface-hover" @click="closeSearchResults($event)">
|
||||
<span class="block text-sm font-medium truncate" style="color: var(--text-primary);">{ r.Title }</span>
|
||||
if r.Category != "" {
|
||||
<span class="block text-xs" style="color: var(--text-secondary);">{ r.Category }</span>
|
||||
|
||||
@@ -92,7 +92,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<!-- Mobile drawer backdrop --><div id=\"docs-overlay\" class=\"hidden fixed inset-0 z-40 lg:hidden\" style=\"background: var(--surface-overlay);\" onclick=\"document.getElementById('docs-sidebar').classList.add('-translate-x-full'); this.classList.add('hidden')\"></div><div class=\"flex\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<!-- Mobile drawer backdrop --><div id=\"docs-overlay\" class=\"hidden fixed inset-0 z-40 lg:hidden\" style=\"background: var(--surface-overlay);\" @click=\"closeDocsSidebar($event)\"></div><div class=\"flex\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -823,7 +823,7 @@ func DocsSearchResults(results []SearchResultItem) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\" class=\"block px-4 py-2 no-underline transition-colors hover:bg-surface-hover\" onclick=\"this.closest('#docs-search-results').classList.add('hidden')\"><span class=\"block text-sm font-medium truncate\" style=\"color: var(--text-primary);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\" class=\"block px-4 py-2 no-underline transition-colors hover:bg-surface-hover\" @click=\"closeSearchResults($event)\"><span class=\"block text-sm font-medium truncate\" style=\"color: var(--text-primary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ templ Progress(user User, progressData []handlers.ProgressWithMedia, errorMessag
|
||||
src={ item.CoverImagePath }
|
||||
alt="Cover"
|
||||
class="w-24 h-32 object-cover rounded-lg"
|
||||
onerror="this.src='/static/placeholder-book.svg'"
|
||||
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
|
||||
@@ -102,7 +102,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" alt=\"Cover\" class=\"w-24 h-32 object-cover rounded-lg\" onerror=\"this.src='/static/placeholder-book.svg'\"></div><div class=\"flex-1 min-w-0\"><div class=\"flex justify-between items-start gap-4 mb-3\"><div class=\"min-w-0\"><a href=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" alt=\"Cover\" class=\"w-24 h-32 object-cover rounded-lg\"></div><div class=\"flex-1 min-w-0\"><div class=\"flex justify-between items-start gap-4 mb-3\"><div class=\"min-w-0\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ templ SeriesCard(series SeriesCardData) {
|
||||
class={ "cover-img cover-" + fmt.Sprintf("%d", i) }
|
||||
alt={ series.Name }
|
||||
loading="lazy"
|
||||
onerror="this.src='/static/placeholder-book.svg'"
|
||||
|
||||
/>
|
||||
}
|
||||
if len(series.CoverPaths) == 0 {
|
||||
|
||||
@@ -323,7 +323,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\"> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" loading=\"lazy\"> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ templ UnlinkedMatchCards(progressId string, matches []services.BookMatch) {
|
||||
src={ match.CoverImagePath }
|
||||
alt="Cover"
|
||||
class="w-16 h-24 object-cover rounded"
|
||||
onerror="this.src='/static/placeholder-book.svg'"
|
||||
|
||||
/>
|
||||
<div>
|
||||
<h5 class="font-semibold" style="color: var(--text-primary)">{ match.Title }</h5>
|
||||
|
||||
@@ -98,7 +98,7 @@ func UnlinkedMatchCards(progressId string, matches []services.BookMatch) templ.C
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" alt=\"Cover\" class=\"w-16 h-24 object-cover rounded\" onerror=\"this.src='/static/placeholder-book.svg'\"><div><h5 class=\"font-semibold\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" alt=\"Cover\" class=\"w-16 h-24 object-cover rounded\"><div><h5 class=\"font-semibold\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user