Formatting was configured but never enforced: `bun run format:check` reported 199 unformatted files and ran in no workflow and in no git hook, so .prettierrc (printWidth 100, trailing commas) described an intention rather than the tree. This is the one-time sweep that makes the check gateable. Whitespace and token-reflow only -- no behavioural change: `bun run check` reports 0 errors and all 1053 frontend tests pass before and after. Kept out of every other commit on purpose. A 199-file diff mixed with real changes is unreviewable, and the next commit turns format:check into a hard CI gate so this cannot silently accumulate again.
84 lines
3.1 KiB
Svelte
84 lines
3.1 KiB
Svelte
<!--
|
|
The header search box (md+ only; below md the bottom-nav Search tab and the
|
|
/search page's own input serve that role).
|
|
|
|
One box, one results surface. The bar renders on the library routes *and on
|
|
/search itself*, so searching from the header no longer swaps you onto a
|
|
screen whose input is somewhere else: the box you typed in stays where it is
|
|
and keeps driving the results. Off /search it navigates there (the only
|
|
surface that renders results); on /search it republishes the query into the
|
|
URL, which the page consumes.
|
|
|
|
TRACES: UR-049, UR-054 | DR-063, DR-064, DR-147
|
|
-->
|
|
<script lang="ts">
|
|
import { onMount, tick } from "svelte";
|
|
import { goto } from "$app/navigation";
|
|
import { page } from "$app/stores";
|
|
import { library } from "$lib/stores/library";
|
|
import Search from "$lib/components/Search.svelte";
|
|
import {
|
|
isSearchRoute,
|
|
parseSearchScope,
|
|
resolveSearchScope,
|
|
searchRouteUrl,
|
|
type SearchScope,
|
|
} from "$lib/utils/searchScope";
|
|
|
|
// Seeded from the URL, then owned by the user. A navigation to /search
|
|
// remounts this component (library and root render their own AppHeader), so
|
|
// reading `?q=` here is what carries a half-typed query across that hop.
|
|
let value = $state($page.url.searchParams.get("q") ?? "");
|
|
let inputEl = $state<HTMLInputElement | null>(null);
|
|
|
|
let scope = $state<SearchScope>(
|
|
isSearchRoute($page.url.pathname)
|
|
? parseSearchScope($page.url.searchParams.get("scope"))
|
|
: resolveSearchScope($page.url.pathname),
|
|
);
|
|
|
|
$effect(() => {
|
|
const url = $page.url;
|
|
if (isSearchRoute(url.pathname)) {
|
|
// On /search the scope chips own the scope and publish it in the URL, so
|
|
// the bar follows rather than overriding it on the next keystroke.
|
|
scope = parseSearchScope(url.searchParams.get("scope"));
|
|
} else if (!value.trim()) {
|
|
// Elsewhere the route seeds the scope, but only while no search is
|
|
// active: navigating must not snap a widened search back to the section
|
|
// the user happens to be in.
|
|
scope = resolveSearchScope(url.pathname);
|
|
}
|
|
});
|
|
|
|
// Landing on /search with a seeded query means the user was mid-type in the
|
|
// previous route's header. That box is gone; put the caret back in this one
|
|
// so their next keystroke lands in the search field and not nowhere.
|
|
onMount(async () => {
|
|
if (!value) return;
|
|
await tick();
|
|
inputEl?.focus();
|
|
inputEl?.setSelectionRange(value.length, value.length);
|
|
});
|
|
|
|
async function handleSearch(query: string) {
|
|
if (isSearchRoute($page.url.pathname)) {
|
|
// Already on the results surface — republish in place. replaceState keeps
|
|
// a whole session of typing to a single history entry.
|
|
await goto(searchRouteUrl(query, scope), {
|
|
replaceState: true,
|
|
keepFocus: true,
|
|
noScroll: true,
|
|
});
|
|
return;
|
|
}
|
|
if (!query.trim()) {
|
|
library.clearSearch();
|
|
return;
|
|
}
|
|
await goto(searchRouteUrl(query, scope));
|
|
}
|
|
</script>
|
|
|
|
<Search bind:value bind:inputEl placeholder="Search your library..." onSearch={handleSearch} />
|