diff --git a/ui/src/lib/components/SearchModal.svelte b/ui/src/lib/components/SearchModal.svelte new file mode 100644 index 0000000..e2725a1 --- /dev/null +++ b/ui/src/lib/components/SearchModal.svelte @@ -0,0 +1,437 @@ + + + + + + + +
{ if (e.target === e.currentTarget) onclose(); }} +> + + +
e.stopPropagation()} + > + +
+ + {#if loading} + + + + + {:else} + + + + {/if} + + { if (e.key === 'Enter') { e.preventDefault(); submitQuery(); } }} + autocomplete="off" + autocorrect="off" + spellcheck={false} + /> + + {#if query} + + {/if} + + +
+ + +
+ + + {#if error} +

{error}

+ + + {:else if visibleResults.length > 0} + +
+

+ {#if localCount > 0 && remoteCount > 0} + {localCount} in library + · {remoteCount} from Novelfire + {:else if localCount > 0} + {localCount} in library + {:else} + {remoteCount} from Novelfire + {/if} +

+ + +
+ + {#each visibleResults as r, i} + {@const genres = parseGenres(r.genres)} + {@const remote = isRemote(r)} + + {/each} + + + + + + {:else if query.trim().length >= 2 && !loading} +
+

No results for "{query.trim()}"

+

Try a different title, author, or browse by genre below.

+
+ {#each GENRE_SUGGESTIONS as genre} + + {/each} +
+
+ + + {:else if query.trim().length === 0} + + {#if recents.length > 0} +
+
+

Recent

+ +
+ {#each recents as r} +
+ + +
+ {/each} +
+
+ {/if} + + +
+

Browse by genre

+
+ {#each GENRE_SUGGESTIONS as genre} + + {/each} +
+
+ {/if} +
+
+
diff --git a/ui/src/routes/+layout.svelte b/ui/src/routes/+layout.svelte index 655c40a..8cbde99 100644 --- a/ui/src/routes/+layout.svelte +++ b/ui/src/routes/+layout.svelte @@ -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 @@ {/if}
+ + {#if !/\/books\/[^/]+\/chapters\//.test(page.url.pathname)} + + {/if} {/if} + + +{#if searchOpen && !listeningModeOpen} + { searchOpen = false; }} /> +{/if} + + { + // 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; + } +}} />