~james/core

core/web/src/lib/components/lines/LineSearch.svelte -rw-r--r-- 2.0 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<script lang="ts">
	import ChevronDown from "$icon/chevron-down";
	import ChevronUp from "$icon/chevron-up";
	import Search from "$icon/search";
	import X from "$icon/x";
	import type { HTMLAttributes } from "svelte/elements";
	import Button from "#lib/components/ui/Button.svelte";
	import Input from "#lib/components/ui/Input.svelte";
	import { MATCH_LIMIT } from "#lib/components/lines/search.ts";

	interface Props extends Omit<HTMLAttributes<HTMLElement>, "class" | "onkeydown"> {
		label: string;
		query?: string;
		total: number;
		current: number;
		input?: HTMLInputElement;
		onstep: (delta: number) => void;
		onclose?: () => void;
		class?: string;
	}

	let {
		label,
		query = $bindable(""),
		total,
		current,
		input = $bindable(),
		onstep,
		onclose,
		class: className,
		...rest
	}: Props = $props();

	const count = $derived(
		query === ""
			? undefined
			: total === 0
				? "No results"
				: `${current + 1}/${total >= MATCH_LIMIT ? `${MATCH_LIMIT}+` : total}`
	);

	const onkeydown = (event: KeyboardEvent) => {
		if (event.key === "Enter") {
			event.preventDefault();
			onstep(event.shiftKey ? -1 : 1);
		} else if (event.key === "Escape" && (onclose || query !== "")) {
			event.preventDefault();
			if (onclose) onclose();
			else query = "";
		}
	};
</script>

<search {...rest} class="flex items-center gap-3xs {className}">
	<Input
		bind:value={query}
		bind:element={input}
		type="search"
		iconLeft={Search}
		suffix={count}
		{label}
		placeholder="Find"
		autocomplete="off"
		spellcheck={false}
		class="min-w-0 flex-1 [&_input::-webkit-search-cancel-button]:appearance-none"
		{onkeydown}
	/>
	<span class="sr-only" role="status" aria-live="polite">{count ?? ""}</span>
	<Button
		variant="ghost"
		icon={ChevronUp}
		aria-label="Previous match"
		disabled={total === 0}
		onclick={() => onstep(-1)}
	/>
	<Button
		variant="ghost"
		icon={ChevronDown}
		aria-label="Next match"
		disabled={total === 0}
		onclick={() => onstep(1)}
	/>
	{#if onclose}
		<Button variant="ghost" icon={X} aria-label="Close search" onclick={onclose} />
	{/if}
</search>