feat: universal search modal
All checks were successful
Release / Test backend (push) Successful in 41s
Release / Check ui (push) Successful in 1m42s
Release / Docker / caddy (push) Successful in 56s
Release / Docker / backend (push) Successful in 2m35s
Release / Docker / runner (push) Successful in 3m37s
Release / Upload source maps (push) Successful in 1m29s
Release / Docker / ui (push) Successful in 2m33s
Release / Gitea Release (push) Successful in 37s
All checks were successful
Release / Test backend (push) Successful in 41s
Release / Check ui (push) Successful in 1m42s
Release / Docker / caddy (push) Successful in 56s
Release / Docker / backend (push) Successful in 2m35s
Release / Docker / runner (push) Successful in 3m37s
Release / Upload source maps (push) Successful in 1m29s
Release / Docker / ui (push) Successful in 2m33s
Release / Gitea Release (push) Successful in 37s
- New SearchModal.svelte: full-screen modal with blurred backdrop
- Live results as you type (300ms debounce, min 2 chars)
- Local vs Novelfire badge on each result card (cover + title + author +
genres + chapter count)
- Local/remote counts shown in result header
- 'See all in catalogue' shortcut button + footer repeat link
- Recent searches (localStorage, max 8, per-item remove + clear all)
- Genre suggestion chips shown when query is empty or no results found
- Keyboard navigation: ArrowUp/Down to select, Enter to open, Escape to close
- Body scroll lock while open
- +layout.svelte:
- Imports SearchModal, adds searchOpen state
- Search icon button in nav header (hidden on chapter reader pages)
- Global keyboard shortcut: '/' or Cmd/Ctrl+K opens modal
- Shortcut ignored when focused in input/textarea or on chapter pages
- Modal not shown while ListeningMode is open
- Auto-closes on route change
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
import { locales, getLocale } from '$lib/paraglide/runtime.js';
|
||||
import ListeningMode from '$lib/components/ListeningMode.svelte';
|
||||
import SearchModal from '$lib/components/SearchModal.svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
let { children, data }: { children: Snippet; data: LayoutData } = $props();
|
||||
@@ -19,6 +20,15 @@
|
||||
// Mobile nav drawer state
|
||||
let menuOpen = $state(false);
|
||||
|
||||
// Universal search
|
||||
let searchOpen = $state(false);
|
||||
|
||||
// Close search on navigation
|
||||
$effect(() => {
|
||||
void page.url.pathname;
|
||||
searchOpen = false;
|
||||
});
|
||||
|
||||
// Desktop dropdown menus
|
||||
let userMenuOpen = $state(false);
|
||||
let langMenuOpen = $state(false);
|
||||
@@ -434,6 +444,20 @@
|
||||
</a>
|
||||
{/if}
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<!-- Universal search button (hidden on chapter/reader pages) -->
|
||||
{#if !/\/books\/[^/]+\/chapters\//.test(page.url.pathname)}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => { searchOpen = true; userMenuOpen = false; langMenuOpen = false; themeMenuOpen = false; menuOpen = false; }}
|
||||
title="Search (/ or ⌘K)"
|
||||
aria-label="Search books"
|
||||
class="flex items-center justify-center w-8 h-8 rounded transition-colors text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2)"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
<!-- Theme dropdown (desktop) -->
|
||||
<div class="hidden sm:block relative">
|
||||
<button
|
||||
@@ -961,3 +985,22 @@
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Universal search modal — shown from anywhere except focus mode / listening mode -->
|
||||
{#if searchOpen && !listeningModeOpen}
|
||||
<SearchModal onclose={() => { searchOpen = false; }} />
|
||||
{/if}
|
||||
|
||||
<svelte:window onkeydown={(e) => {
|
||||
// Don't intercept when typing in an input/textarea
|
||||
const tag = (e.target as HTMLElement).tagName;
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement).isContentEditable) return;
|
||||
// Don't open on chapter reader pages
|
||||
if (/\/books\/[^/]+\/chapters\//.test(page.url.pathname)) return;
|
||||
if (searchOpen) return;
|
||||
// `/` key or Cmd/Ctrl+K
|
||||
if (e.key === '/' || ((e.metaKey || e.ctrlKey) && e.key === 'k')) {
|
||||
e.preventDefault();
|
||||
searchOpen = true;
|
||||
}
|
||||
}} />
|
||||
|
||||
Reference in New Issue
Block a user