OmniboxSuggestions.svelte RawHistoryFolder
1<!--
2 Omnibox Suggestions Component
3
4 Displays search suggestions with favicons and URL highlighting
5-->
6<script lang="ts">
7 import { omniboxStore } from './stores.svelte.ts';
8 import { omniboxBridge } from './messaging';
9
10 // Reactive state
11 let suggestions = $derived(omniboxStore.suggestions);
12 let selectedIndex = $derived(omniboxStore.selectedIndex);
13 let hasContent = $state(false);
14
15 // Update hasContent when suggestions change
16 $effect(() => {
17 hasContent = omniboxStore.mode === 'omnibox' && suggestions.length > 0;
18 });
19
20 // Handle suggestion item mouse enter
21 function handleItemMouseEnter(index: number) {
22 omniboxStore.setSelectedIndex(index);
23 omniboxStore.setFaded(false);
24 scrollToSelection();
25 }
26
27 // Handle suggestion item click
28 function handleItemClick(suggestion: any) {
29 omniboxBridge.navigate(suggestion.url);
30 omniboxStore.close();
31 }
32
33 // Handle keyboard activation on suggestion item
34 function handleItemKeyDown(event: KeyboardEvent, suggestion: any) {
35 if (event.key === 'Enter' || event.key === ' ') {
36 event.preventDefault();
37 handleItemClick(suggestion);
38 }
39 }
40
41 // Scroll list to show selected item
42 function scrollToSelection() {
43 const selectedItem = document.getElementById(`omnibox-item-${selectedIndex}`);
44 if (selectedItem && selectedItem.scrollIntoView) {
45 try {
46 selectedItem.scrollIntoView({ block: 'nearest' });
47 } catch {
48 selectedItem.scrollIntoView();
49 }
50 }
51 }
52
53 // Parse URL for display
54 function parseUrl(url: string) {
55 try {
56 const urlObj = new URL(url, window.location.href);
57 return {
58 domain: urlObj.hostname,
59 path: (urlObj.pathname || '') + (urlObj.search || '') + (urlObj.hash || '')
60 };
61 } catch {
62 return {
63 domain: url || '',
64 path: ''
65 };
66 }
67 }
68
69 // Handle favicon error
70 function handleFaviconError(event: Event) {
71 const target = event.target as HTMLImageElement;
72 const chip = target.parentElement;
73 if (chip) {
74 chip.style.display = 'none';
75 }
76 }
77
78 // Watch for selection changes to scroll
79 $effect(() => {
80 if (selectedIndex >= 0) {
81 scrollToSelection();
82 }
83 });
84</script>
85
86{#if hasContent}
87 <div
88 id="omnibox-list"
89 class="suggestion-list"
90 role="listbox"
91 aria-label="Search suggestions"
92 >
93 {#each suggestions as suggestion, index (suggestion.url)}
94 {@const { domain, path } = parseUrl(suggestion.url)}
95 {@const isSelected = index === selectedIndex}
96
97 <div
98 id="omnibox-item-{index}"
99 class={isSelected ? 'suggestion-item selected' : 'suggestion-item'}
100 role="option"
101 tabindex={isSelected ? 0 : -1}
102 aria-selected={isSelected}
103 onmouseenter={() => handleItemMouseEnter(index)}
104 onclick={() => handleItemClick(suggestion)}
105 onkeydown={(e) => handleItemKeyDown(e, suggestion)}
106 >
107 <!-- Favicon chip -->
108 {#if suggestion.favicon}
109 <div class="suggestion-favicon">
110 <img
111 src={suggestion.favicon}
112 alt=""
113 class="suggestion-favicon-img"
114 loading="lazy"
115 onerror={handleFaviconError}
116 style="image-rendering: -webkit-optimize-contrast;"
117 />
118 </div>
119 {/if}
120
121 <!-- URL text with gradient fade -->
122 <div class="suggestion-text">
123 <!-- Domain -->
124 <span class="suggestion-domain">
125 {domain}
126 </span>
127
128 <!-- Separator -->
129 <span class="suggestion-separator">
130 {' | '}
131 </span>
132
133 <!-- Path -->
134 <span class="suggestion-path">
135 {path || '/'}
136 </span>
137 </div>
138 </div>
139 {/each}
140 </div>
141{/if}
142
143
144
145<style>
146 .suggestion-list {
147 margin-top: 0.75rem;
148 max-height: 50vh;
149 overflow-y: auto;
150 border-top: 1px solid var(--dynamic-border);
151 background: color-mix(in srgb, var(--dynamic-bg) 94%, var(--dynamic-surface) 6%);
152 }
153
154 .suggestion-item {
155 display: flex;
156 gap: 0.75rem;
157 align-items: center;
158 padding: 0.65rem 0.85rem;
159 border-bottom: 1px dashed var(--dynamic-border);
160 cursor: pointer;
161 transition: background-color 120ms ease, color 120ms ease;
162 letter-spacing: 0.04em;
163 }
164
165 .suggestion-item:last-child {
166 border-bottom: none;
167 }
168
169 .suggestion-item.selected,
170 .suggestion-item:hover,
171 .suggestion-item:focus-visible {
172 background: color-mix(in srgb, var(--dynamic-bg) 60%, var(--dynamic-surface) 40%);
173 color: var(--dynamic-text);
174 outline: none;
175 }
176
177 .suggestion-item .suggestion-domain {
178 color: var(--dynamic-text);
179 opacity: 0.95;
180 }
181
182 .suggestion-item .suggestion-separator {
183 color: var(--dynamic-muted);
184 }
185
186 .suggestion-item .suggestion-path {
187 color: var(--dynamic-muted);
188 }
189
190 .suggestion-favicon {
191 width: 20px;
192 height: 20px;
193 border: 1px solid var(--dynamic-border);
194 display: flex;
195 align-items: center;
196 justify-content: center;
197 background: var(--dynamic-bg);
198 }
199
200 .suggestion-favicon-img {
201 width: 16px;
202 height: 16px;
203 object-fit: contain;
204 }
205
206 .suggestion-text {
207 flex: 1;
208 min-width: 0;
209 display: flex;
210 gap: 0.75rem;
211 align-items: center;
212 white-space: nowrap;
213 overflow: hidden;
214 text-overflow: ellipsis;
215 mask-image: linear-gradient(90deg, black 85%, transparent 100%);
216 -webkit-mask-image: linear-gradient(90deg, black 85%, transparent 100%);
217 }
218</style>