Connect the existing progress_mode setting dropdown to the reader's
progress display. Four modes are now functional:
- pages: overall percent + page/location number (default, existing)
- chapter: chapter title + page X / Y within current section
- percentage: overall percent only
- time-left: percent + estimated time remaining via reading speed API
The progress display in the bottom bar is now clickable to cycle through
modes with immediate visual feedback. The settings dropdown is bound
with x-model for persistence. Reading speed is fetched once on init
from the backend reading-speed API for time-left estimates.
Regenerated all _templ.go files after running templ generate. Changes
include updated FileName references (relative path normalization) and
line number adjustments from the templ code generator.
Fix two bugs in progress display across book detail, progress page, reader,
and sync modal templates:
1. Percentage was stored as 0.0-1.0 fraction but displayed as-if 0-100
(showing 0.5% instead of 50%). Multiply by 100 at the data source in
both GetAllProgress and GetAllProgressData handlers, and in the reader
route's ReadingProgress construction.
2. Progress bar width was never evaluated — { expr } inside style=".."
was rendered as literal text by templ, resulting in 0% width bars for
all items. Fixed by using templ's style={ expr } attribute syntax
which evaluates the Go expression (uses SanitizeStyleAttributeValues).
Also add format-aware progress display:
- Reader template: shows "45% · Page 89/196" for reflowable (estimated
pages), "127/342" for comics/PDFs (actual pages)
- Progress page: shows "Page X of Y (est.)" for reflowable, "X / Y"
for fixed layout
- Add FormatGroup and EstimatedPages to ProgressWithMedia struct
- Remove hardcoded totalPages=200 fallback in progress handler (now 0)
- Add fmt import to progress.templ for string formatting
Add fields to ReaderMetadata and ReadingProgress template types to support
KOReader-like progress display:
ReaderMetadata:
- TotalCharacters: from media item, used for estimated page calculation
- EstimatedPages: computed via sync.EstimatedPages()
ReadingProgress:
- Chapter: current chapter index from reading_progress
- ChapterProgress: within-chapter progress (0-100, multiplied from DB fraction)
- FormatGroup: item format for conditional display logic
These fields enable format-aware progress display (pages for comics/PDFs,
estimated pages for reflowable, percentage for all).
The web reader had all the infrastructure for progress persistence
(updateReadingProgress/getReadingProgress API functions, PUT/GET
endpoints, database queries) but the reader.ts never called them.
Changes:
- Add debounced (2s) saveProgress call on every relocate event that
PUTs percentage, current_page, total_pages, and epubcfi to the
existing /api/media-items/:id/progress endpoint
- Replace renderer.next() with view.init({ lastLocation }) to restore
the reader to the last saved position on load (CFI first, then
fraction fallback, then default first page)
- Pass savedPercentage and savedCfi from server-side progress data
through readerInitExpr config to the JS initReader function
- Add mediaItemId and saveTimeout to the Alpine data object
This fixes both the blank /progress page and the missing progress
section on book detail pages — both were empty because the
reading_progress table never received any data from the web reader.
The reader failed to load comics and manga (and any file with special
characters in its path) for two reasons:
1. FileURL was built with raw fmt.Sprintf instead of ResolveMediaURL,
so characters like '#' in paths (e.g. 'Annual #2') were interpreted
as URL fragments, truncating the path and causing 404s.
2. The Alpine x-init expression used raw string interpolation for config
values, so apostrophes in paths (e.g. "I'll Use My Appraisal Skill")
broke JavaScript parsing with 'Unexpected identifier'.
Fix by using utils.ResolveMediaURL for proper URL path encoding and
json.Marshal for the initReader config to safely escape all special
characters.
The back link in ReaderChrome used literal curly braces inside the href
attribute string (href="/media-items/{ metadata.MediaItemID }") which
doesn't interpolate the variable in templ. Changed to use the correct
templ expression syntax: href={ "/media/" + metadata.MediaItemID }.
Also fixed minor formatting issues:
- Normalize whitespace in comment after closing div
- Collapse empty navigator-viewport div to single line
- Remove Playwrite NZ Guides test font and all references (FONT_MAP,
FONT_FILES, reader-fonts.css, dropdown option, font files)
- Move settings panel from left sidebar to right sidebar (left sidebar
now only contains TOC)
- Move Restore Defaults button from top bar icon to a styled button
inside the settings panel, side by side with Done button
The paginator renders inside a sandboxed iframe that blocks @font-face
URL fetches. Fonts were never loading — all font-family rules fell back
to the generic 'serif' system font, making every reading font identical.
Fix: fetch font files on the parent page, create blob: URLs via
URL.createObjectURL(), and use those blob URLs in the @font-face rules
injected into the iframe via setStyles(). Blob URLs are always
same-origin with the creating document, so the sandboxed iframe can
access them with allow-same-origin.
Also added Playwrite NZ Guides as a test font for verifying font
switching works.
Fonts weren't loading because the paginator renders inside a sandboxed
iframe. @font-face declarations in the parent page's CSS are invisible
to the iframe's document. Even injecting @font-face rules via
setStyles() may not trigger font loading in sandboxed iframes.
Fix: inject a <link> to reader-fonts.css directly into the iframe's
document on each section load, so @font-face declarations are parsed
in the iframe's own document context where font-family rules can
reference them.
Also:
- Remove foliate-themes.css entirely (no longer needed)
- Set viewport background color directly via JS using THEME_COLORS map
- Remove reading theme CSS classes from viewport element
Comic Sans MS is a system font not available on Linux. The cursive
fallback rendered as a script font, giving false negatives. All 8
loaded reading fonts are serif fonts loaded via @font-face, so they
intentionally look similar — font switching is confirmed working.
- Add Comic Sans MS as a test font option to verify font switching works
- Add !important to background-color and color in getCSS() to prevent
book CSS from overriding user's reading theme colors
- Expand font size range from 12-24px to 10-40px, bump default to 18px
- Expand line height range from 1.0-2.5 to 0.8-3.0
- Add restoreDefaults() method that resets reading theme, font, size,
line height, and justify/hyphenate to sensible defaults
- Add ↩️ restore defaults button in top bar underneath the settings gear
Two root causes fixed:
1. Reading theme CSS variables were on document.body, leaking font/color
into chrome UI. Now scoped to #reader-viewport so chrome keeps its own
theme (system font, --text-primary colors) while the reading area uses
reading theme colors/background.
2. getCSS() never received font family, font size, or line height settings.
The settings UI (dropdowns, sliders) saved values but they were never
injected into the book's shadow DOM. Now getCSS() accepts all four
settings and generates proper CSS rules for them.
Changes:
- Wrap foliate-view in #reader-viewport div (absolute positioned between
chrome bars)
- getCSS() reads computed style from #reader-viewport, not document.body
- getCSS() params expanded: fontFamily, fontSize, lineHeight, justify,
hyphenate (removed unused 'spacing')
- Added FONT_MAP to translate setting keys to CSS font-family values
- applyTheme() targets #reader-viewport instead of document.body
- Removed dead #reader-viewport typography rules from foliate-themes.css
(shadow DOM doesn't inherit outer styles), kept only background-color
- Position foliate-view with absolute inset-x-0 top-[52px] bottom-[52px]
so book content renders between the fixed header and footer bars instead
of behind them
- Confirmed removal of foliate-themes.css base typography was correct:
body color now comes from chrome theme's --text-primary (light for
dark chrome themes like tokyo-night)
Two fixes:
1. Remove base typography block from foliate-themes.css. The html/body
rules were unlayered CSS that overrode the chrome theme's layered
body styles, causing dark reading theme text colors (--reader-text)
to apply to the outer chrome UI on dark backgrounds. These styles
are only meant for the shadow DOM, which getCSS() already handles.
2. Move missing typography rules (img, blockquote, a, p orphans/widows)
into getCSS() so the shadow DOM still gets them.
3. Add sun/moon emoji indicators to the light/dark toggle switch.
Add explicit light/dark mode toggle switch to the reading theme settings.
The reading mode defaults based on the chrome theme (dark chrome themes
like tokyo-night default to dark reading mode).
- Add readingMode property to readerShell Alpine component
- Add toggleReadingMode() method that toggles dark class on body
- Add detectChromeDarkMode() to infer default from chrome theme
- Update applyTheme() to add/remove dark class and persist reading_mode
- Add toggle switch UI in settings panel (blue pill style, next to
Reading Theme heading)
- Add reading_mode to default settings in settings-manager
The foliate-view custom element had no height, causing its shadow DOM
content to collapse to 0px. Books were loading but invisible.
- Add h-screen overflow-hidden to body for full viewport height
- Add block w-full h-full to foliate-view element
- Replace flex-grow with Tailwind grow class on progress slider
- Remove #progress-slider CSS rule from inline style block
(replaced by Tailwind grow utility)
Major rewrite of the web reader to properly interface with
@bookhoard/foliate-js, replacing the abandoned panel-detection
architecture with direct pan and zoom support built into the
foliate-js FixedLayout renderer.
Template (reader.templ):
- Fix critical bug: x-init config was using literal strings
'{ readerData.X }' inside a quoted attribute, which templ
treated as raw text and never interpolated. Values were never
actually passed to JavaScript. Now uses fmt.Sprintf() with
templ's expression attribute syntax ={ }.
- Pass fileUrl from server so foliate-js can open books directly.
- Redesign bottom bar with foliate-js parity: left/right navigation
buttons, progress slider with tick marks, and zoom controls
(zoom out, percentage display, zoom in, magnifier, pan/select
mode toggle for PDFs).
- Remove panel editor button and enablePanelDetection config.
- Add SVG icon styles for consistent reader controls.
Go types (templates/types.go):
- Expand ReaderMetadata with FormatGroup, MangaType,
ReadingDirection, FileURL, and LibraryID fields needed by
the reader frontend.
Router (internal/router/reader.go):
- Populate new ReaderMetadata fields from database values.
- Construct FileURL from library ID and file path for the
/uploads/library-{id}/* file serving route.
Reader JS (reader.ts):
- Full rewrite modeled on foliate-js Reader class, adapted for
Alpine.js. Opens books via view.open(fileUrl), accesses
view.renderer for zoom/pan/navigation, and wires up keyboard
shortcuts (+/-/0 for zoom, arrows for nav, Escape for magnifier).
- Uses view.isFixedLayout instead of importing FixedLayout class,
avoiding a TypeScript module resolution issue with the Vite alias.
Settings manager (settings-manager.ts):
- Remove dependency on deleted ReaderContext event bus.
- Export loadSettings/saveSettings/syncSettings directly as
standalone async functions.
Cleanup:
- Delete reader-context.ts and reader-events.ts (over-engineered
event system replaced by direct function calls).
- Remove panel_zoom_enabled from ReaderSettings type.
Update the reader template to support dynamic configuration and manga metadata:
templates/reader_templ.go:
- Remove direct foliate-js/view.js script tag (integrated into reader.js)
- Add foliate-themes.css stylesheet for theming support
- Update initReader() call to accept configuration object with:
- mediaItemId: Unique media item identifier
- title: Media item title
- enablePanelDetection: Boolean for comic/manga panel detection
- libraryType: Media type for reader initialization
- formatGroup: Format category (ebook, comic, manga)
- mangaType: Manga subtype for specialized handling
- readingDirection: RTL/LTR/vertical reading direction
- Simplify theme to always use tokyo-night (theme handled in JS)
These changes enable the reader to dynamically configure itself based on
media item metadata, supporting enhanced manga reading features and
panel detection for comic formats.
Add admin_processing_issues_templ.go template for managing processing issues
in the admin dashboard. This template provides:
- List view of all processing issues with filtering by severity
- Issue details display (file path, error type, description)
- Actions to resolve or dismiss issues
- Integration with the ProcessingIssuesHandler API endpoints
This UI enables administrators to monitor and address processing errors that
occur during media scanning and import workflows.
Add ProcessingIssueData and IssueStats types to templates/types.go for use
in the processing issues management UI. These types support:
- ProcessingIssueData: Individual issue details including ID, media item,
file path, format, issue type, severity, and timestamps
- IssueStats: Aggregated counts of issues by severity (error, warning, info)
These types enable the admin UI to display processing issues from the database
and provide statistics for the issues dashboard.
- Update reader initialization to pass panel detection configuration
- Include enablePanelDetection, libraryType, formatGroup params
- Add mangaType and readingDirection for proper manga rendering
- Change theme class to theme-tokyo-night for consistent styling
- Add ProcessingIssuesHandler with List and GetStats methods
- Add AdminProcessingIssues template for issues dashboard
- Display error/warning/info stats cards
- Sort issues by severity and creation date
- Add dismiss functionality for warnings and info items
- Add navigate to media item functionality
- Show issue type, description, and media details
The Read Now button now properly navigates to the reader page at
/readers/{book_id} instead of showing a placeholder alert. Also includes
generated template variable adjustments.
- Change main.js script to use type=module for proper ES module loading
- Add reader-fonts.css link for custom reading fonts
- Add tabindex=0 to reader-content for keyboard accessibility
- Fix metadata.MediaItemID reference in back link
- Add panel editor button for comics/manga
Change script loading from deprecated 'defer' attribute to 'type=module'
for main.js across all templates. This ensures proper ES module loading
and is required for the reader module system to work correctly.
- Export getDefaultSettings function for use in reader-navigation
- Minor formatting improvements in settings-manager.ts
- Add tabindex and ebook-content class to reader-content in template
- Change script tags from 'defer' to 'type=module' for ES module support
- Add reader.js script to reader.templ for the reader bundle
- Update book_detail.templ 'Read Now' button to link to /readers/{id}
- Remove trailing blank line from register.templ
- templates/reader.templ: add script tag to load main.js
- Enables reader shell and Alpine components to initialize
- internal/handlers/reader.go: add GetMediaReadingProgress handler
- Backend now supports fetching reading progress via API
- GET /api/media-items/:id/progress returns current progress
- Fix function signatures in reader.go (c echo.Context -> c *echo.Context)
- Replace non-existent UserHasLibraryAccess with GetUserVisibleLibraries pattern
- Implement SSR reader route in router/reader.go with proper access control
- Add inline library access checking following existing codebase patterns
- Fix ReadingProgress struct to use LastReadAt instead of CreatedAt/UpdatedAt
- Ensure all reader endpoints use consistent library access validation
This provides the backend foundation for the reader feature with proper
access control and SSR rendering capabilities.
- Create templates/reader_templ.go with main Reader() function
- Implement reader shell with top/bottom chrome bars
- Add dockable panels: TOC, Settings (left side)
- Add dockable panels: Navigator, Bookmarks (right side)
- Include panel lock and window-shade toggle buttons
- Support all media types: ebook, comic, manga, pdf
- Initialize reader shell via Alpine.js data attribute
Implement comprehensive comic metadata display features on the SSR book detail
page, supporting all 8 metadata fields from ComicInfo.xml and other sources.
## Template Changes (book_detail.templ)
### Step 1: Reading Direction Badge
- Display directional badge (RTL, LTR, VERTICAL) for manga/comics
- Uses 📖 icon with uppercase direction text
- Auto-hides when direction is "auto" (default)
- Styled with accent color for visibility
### Step 2: Community Rating Display
- Show pre-existing community rating from metadata (0.0-10.0 scale)
- Distinct from user ratings with visual differentiation
- Uses renderStars() helper for visual star display
- Shows both stars and numeric score (e.g., "★★★★☆ 8.5 / 10")
- Smaller, subtler styling than user rating
### Step 3: Comic-Specific Badges
- Age Rating: Content maturity indicator
- Black & White: Visual style badge
- Story Arc: Narrative arc name with 📚 icon
- Badges styled as pills with subtle borders
- Only display when values are present
### Step 4: Universal Series Info
- Series Count: Total items in series
- Volume: Volume/omnibus number
- Imprint: Publisher imprint (e.g., Vertigo)
### Step 5: Comic-Specific Metadata
- Manga Type: Raw/Comic/Manga classification
- Scan Information: Scanner group, resolution
- Alternate Series: Different series numbering
### Step 6: Summary Section
- Display ComicInfo.xml summary when present
- Separate from description field
- Sanitized HTML output with bluemonday
- Scrollable container for long summaries
### Step 7: Metadata Notes
- Technical notes from metadata files
- Internal/useful information (scanner, source, etc.)
- Card-style display with clear typography
### Step 8: Web URL Link
- External link to info sources (Goodreads, ComicVine, etc.)
- Opens in new tab with security attributes
- Displays clean domain name
## Utils Changes (templates/utils.go)
Added helper functions:
- getAlternateSeries(): Extract alternate series from JSONB
- getDomainName(): Extract clean domain for display
- formatAlternateInfo(): Format readable alternate series string
## Implementation Plan
Updated FRONTEND_IMPLEMENTATION_PLAN_COMIC_METADATA.md with:
- Disabled markdownlint for MD013 (line length)
- Added spacing for readability
## Technical Details
- All fields use pgtype.Text/Int4/Bool for NULL handling
- Template conditionals check Valid flag before accessing values
- Consistent styling using CSS custom properties
- HTML escaping for security (except summary with bluemonday)
- Responsive design with mobile-friendly layouts
Related: Database schema already supports all metadata fields
Improve star rating display on book detail page to show always-visible
5-star rating scale with theme-aware colors and visual half-star rendering.
Changes:
Enhanced renderStars() function:
- Always displays 5 stars (0/5 now shows 5 grey stars instead of empty)
- Filled stars use var(--accent) color (theme-aware highlight)
- Empty stars use var(--text-secondary) (theme-aware grey, adapts to light/dark themes)
- Half-stars use CSS linear-gradient (90deg) to split star vertically:
- Left half: var(--accent) (filled, color)
- Right half: var(--text-secondary) (empty, grey)
- Uses webkit-background-clip and text-fill-color transparent for gradient effect
Added getBookRating() helper function:
- Returns rating value or 0 if book.Rating is nil
- Allows unrated books to display 0/5 (5 grey stars)
- Makes rating section always visible instead of hiding when nil
Template changes:
- Updated rating display to always show (no conditionals)
- Removed text-yellow-400 class (colors now inline with theme vars)
- Added templ.Raw() wrapper for HTML rendering (prevents escaping)
- Simplified rating display logic
Benefits:
- Users can now see rating scale even when book isn't rated
- Visual half-star is much more intuitive than ½ text character
- Theme-aware colors adapt to light/dark mode automatically
- Follows existing patterns ( UnsafeHTML, CSS variables, etc.)
This makes the rating section more discoverable and user-friendly.
Add clickable links to book detail page (/media/:uuid) from:
- Dashboard: BookCard components now link to detail page
- Changed from data-action pattern to direct <a> tags
- Removes unused viewBook() function and switch case
- Follows progressive enhancement (works without JS)
- Collections: Book titles link to detail page
- Books displayed in collection detail view
- Progress: Book titles link to detail page
- Progress cards now have clickable title links
All links use direct navigation for better UX and progressive enhancement.
Book detail page can display comprehensive metadata, reading progress,
sync status, and external service links.
Implement SSR-first book detail page at /media/:uuid with complete
book information, progress tracking, and interactivity.
Features:
- Cover image (256x384px) with responsive layout
- Complete metadata: title, author, description, publisher, ISBN,
language, edition, page count, genre, copyright year, format
- External service links (Goodreads, Open Library, Google Books, Amazon)
with smart URL fallback: ID → ISBN → Title+Author
- Reading progress display with device sources (web/kobo/koreader)
- Sync progress modal for conflict resolution
- Collections display as clickable badges
- Notes/highlights counter with placeholder modal
- Rating display (1-10 scale with star rendering)
- HTML sanitization for book descriptions using bluemonday
Data Structure:
- handlers.MediaDetail embeds database.MediaItems for zero duplication
- Uses existing database queries (GetMediaItem, GetMediaRating, etc.)
- Follows project pattern: no parallel type systems
Frontend:
- TypeScript modal triggers (book-detail.ts)
- Alpine.js for modal interactions
- TailwindCSS styling with theme variables
- Responsive: cover-left layout, mobile stacks vertically
Backend:
- Route: GET /media/:uuid (protected)
- Handler: inline function in frontend.go following existing pattern
- Template: SSR-first with progressive enhancement
- Returns HTML only (API uses separate /api/media-items/:id endpoint)
Files created:
- internal/handlers/media_detail.go
- templates/book_detail.templ
- templates/book_detail_modals.templ
- web/src/book-detail.ts
Files modified:
- internal/router/frontend.go (add route)
- web/src/main.ts (import module)
Add utility functions for rendering book metadata:
- renderStars(): Convert rating (1-10 scale) to star display
- formatFileSize(): Convert bytes to human-readable format (KB, MB, GB)
- getExternalURL(): Generate URLs for external book services
with smart fallback: ID → ISBN → Title+Author search
Supports Goodreads, Open Library, Google Books, Amazon
These helpers make book detail template cleaner and follow DRY principle.
Update 'Manage Folders' button text to 'Manage Libraries and Folders'
to better reflect the full functionality of managing both libraries
and scan directories in one place.
Changed the 4 default system collection names from kebab-case to Title Case
with spaces for better readability and professional appearance:
Changes:
- "continue-reading" → "Continue Reading"
- "recently-added" → "Recently Added"
- "recently-read" → "Recently Read"
- "not-started" → "Not Started"
Implementation details:
- Collection Name field: Updated to Title Case (user-visible identifier)
- QueryType field: Unchanged, remains kebab-case (internal switch/case logic)
- All map keys updated to use new Title Case names as lookups
- Restore modal option values updated to match new names
Files modified:
- internal/handlers/auth.go: Default collection creation for new users
- internal/handlers/dashboard.go: Restore endpoint validation map
- internal/services/dashboard_service.go: System collection metadata map
- templates/restore_system_collection_modal.templ: Form option values
Benefits:
- Cleaner, more professional display names for end users
- Consistent with existing restore modal UI labels
- Improved user experience with properly formatted collection names
- Internal QueryType identifiers remain unchanged for code logic
Fixed multiple issues preventing the collections page and modals from working correctly:
1. Alpine Expression Error on page load:
- Added missing semicolon between function calls in x-init directive
- Added missing parentheses to initializeCollectionWebSocket() call
- Collections page now loads without JavaScript errors
2. Modal container removal bug:
- Fixed closeCollectionModal() removing #modal-container parent element
- Changed from modal.parentElement.remove() to modal.remove()
- Modal can now be opened and closed multiple times without errors
- Fixes htmx:targetError when trying to open modal after first close
3. Emoji grid display:
- Modal now properly preserves container across open/close cycles
- setupHTMXModalInit() can successfully repopulate icon grid
- Emoji picker displays correctly on all modal opens
Technical details:
- templates/collections.templ: Fixed x-init syntax errors
- web/src/collections.ts: Fixed modal close logic to preserve container
- Modal container persists across HTMX swaps, allowing repeated use
The load filter dropdown was being cut off when the button was positioned
on the left side of the screen due to static right-0 alignment. This became
more problematic as the button position changes with window resize.
Changes:
- Added dynamic dropdown alignment calculation based on button position
and available viewport space
- Implemented smart positioning logic that checks available space on both
left and right sides before deciding alignment
- Added window resize listener using requestAnimationFrame to dynamically
update dropdown position while open
- Added data-load-filter-btn attribute for reliable DOM querying
- Changed from static right-0 to dynamic :class binding for left/right
alignment
Technical details:
- Alpine.js state: dropdownAlign tracks current alignment (left/right)
- calculateAlignment() method computes button position and available space
- Uses getBoundingClientRect() to measure button position relative to viewport
- Prefers side with >=320px space, otherwise chooses larger side
- requestAnimationFrame ensures smooth updates during resize without
performance degradation
Fixes issue where dropdown extends beyond viewport edge when button
is near left or right edge of screen.
Template changes for bookshelf page:
Cover filter (tristate button):
- Replace checkbox with 3-state button: Any (null) → Has Cover → No Cover
- Add Alpine state management for hasCoverState (true/false/null)
- Button shows dynamic icon and label based on state:
- ○ Cover: Any
- ✓ Has Cover
- ✗ No Cover
- Hidden input conditionally rendered by Alpine (x-if="hasCoverState !== null")
- Only submits "true"/"false" or not at all, never empty string
- Theme-aware styling using CSS variables and color-mix()
Filter management improvements:
- Add name="library" attribute to library select for proper form submission
- Create filter_item.templ component for rendering individual filter items
- Add Load Filter and Delete Filter buttons with Alpine event handlers
- Update save filter form to use HTMX attributes:
- hx-post, hx-target, hx-swap, hx-include for AJAX submission
- @htmx:afterRequest event for modal cleanup
This fixes issues where:
- Library wasn't being submitted with search/filter requests
- has_cover was sending empty string causing no results
- Saved filters couldn't be loaded or deleted
Restructured the bookshelf filter form to be a proper visible form
instead of individual inputs with HTMX attributes pointing to a
hidden form.
Changes:
- Wrapped all filter inputs in a visible <form id="filter-form">
with hx-get="/api/media-items/search" and hx-target="#books-grid"
- Removed redundant HTMX attributes from individual inputs since
they're now part of the form
- Added "Search" submit button to explicitly trigger form submission
- Moved hidden pagination state inputs (limit, offset) inside the form
- Preserved all existing functionality: autocomplete, fuzzy search,
saved filters, clear filters button
- Added checked attribute to has_cover checkbox for default state
This change fixes the architectural issue where filter inputs were
outside the form and relied on hx-include, which was fragile and
made form handling complex. The new structure is more maintainable
and follows standard HTML form patterns.
The form now properly includes all filter parameters when submitted,
ensuring that search, filters, and pagination work correctly together.