OmniboxFind.svelte RawHistoryFolder
1<!--
2 Omnibox Find Component
3
4 Displays find results and match navigation
5-->
6<script lang="ts">
7 import { omniboxStore } from './stores.svelte.ts';
8 import { revealMatch } from './find';
9
10 // Reactive state
11 let matches = $derived(omniboxStore.matches);
12 let selectedIndex = $derived(omniboxStore.selectedIndex);
13 let hasContent = $state(false);
14
15 // Update hasContent when matches change
16 $effect(() => {
17 hasContent = omniboxStore.mode === 'find' && matches.length > 0;
18 });
19
20 // Computed total count
21 let totalMatches = $derived(matches.length);
22
23 // Handle match item mouse enter
24 function handleItemMouseEnter(index: number) {
25 omniboxStore.setSelectedIndex(index);
26 omniboxStore.setFaded(true);
27 scrollToSelection();
28 }
29
30 // Handle match item click
31 function handleItemClick(index: number) {
32 omniboxStore.setSelectedIndex(index);
33 revealMatch(index);
34 omniboxStore.close();
35 }
36
37 // Scroll list to show selected item
38 function scrollToSelection() {
39 const selectedItem = document.getElementById(`find-item-${selectedIndex}`);
40 if (selectedItem && selectedItem.scrollIntoView) {
41 try {
42 selectedItem.scrollIntoView({ block: 'nearest' });
43 } catch {
44 selectedItem.scrollIntoView();
45 }
46 }
47 }
48
49 // Watch for selection changes to scroll
50 $effect(() => {
51 if (selectedIndex >= 0) {
52 scrollToSelection();
53 }
54 });
55</script>
56
57<div id="find-list" class="find-list" role="listbox" aria-label="Find results">
58 <!-- Header with match count -->
59 <div class="find-header">
60 {#if totalMatches > 0}
61 {totalMatches} match{totalMatches === 1 ? '' : 'es'}
62 {:else}
63 No matches
64 {/if}
65 </div>
66
67 <!-- Match results -->
68 {#if hasContent}
69 {#each matches as match, index (index)}
70 {@const isSelected = index === selectedIndex}
71
72 <div
73 id="find-item-{index}"
74 class={isSelected ? 'find-item selected' : 'find-item'}
75 role="option"
76 tabindex="-1"
77 aria-selected={isSelected}
78 onmouseenter={() => handleItemMouseEnter(index)}
79 onclick={() => handleItemClick(index)}
80 onkeydown={(e) => e.key === 'Enter' && handleItemClick(index)}
81 >
82 <!-- Match context -->
83 <div class="find-context">
84 {match.context || ''}
85 </div>
86 </div>
87 {/each}
88 {/if}
89</div>
90
91<style>
92 .find-list {
93 margin-top: 0.75rem;
94 max-height: 50vh;
95 overflow-y: auto;
96 border-top: 1px solid var(--dynamic-border);
97 background: color-mix(in srgb, var(--dynamic-bg) 94%, var(--dynamic-surface) 6%);
98 }
99
100 .find-header {
101 padding: 0.65rem 0.85rem;
102 font-size: 0.7rem;
103 text-transform: uppercase;
104 letter-spacing: 0.08em;
105 color: var(--dynamic-muted);
106 border-bottom: 1px dashed var(--dynamic-border);
107 }
108
109 .find-item {
110 padding: 0.65rem 0.85rem;
111 border-bottom: 1px dashed var(--dynamic-border);
112 cursor: pointer;
113 transition: background-color 120ms ease, color 120ms ease;
114 }
115
116 .find-item:last-child {
117 border-bottom: none;
118 }
119
120 .find-item.selected,
121 .find-item:hover,
122 .find-item:focus-visible {
123 background: color-mix(in srgb, var(--dynamic-bg) 75%, var(--dynamic-surface) 25%);
124 color: var(--dynamic-text);
125 outline: none;
126 }
127
128 .find-context {
129 color: var(--dynamic-text);
130 font-size: 0.75rem;
131 white-space: nowrap;
132 overflow: hidden;
133 text-overflow: ellipsis;
134 }
135</style>