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(true);
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="mt-2 max-h-[50vh] overflow-auto border-t border-[#333]"
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="px-2.5 py-2 flex gap-2.5 items-center cursor-pointer
100 border-b border-[#2a2a2a] last:border-b-0
101 {isSelected ? 'bg-[#0a0a0a]' : ''}"
102 role="option"
103 tabindex={isSelected ? 0 : -1}
104 aria-selected={isSelected}
105 onmouseenter={() => handleItemMouseEnter(index)}
106 onclick={() => handleItemClick(suggestion)}
107 onkeydown={(e) => handleItemKeyDown(e, suggestion)}
108 >
109 <!-- Favicon chip -->
110 {#if suggestion.favicon}
111 <div
112 class="flex-shrink-0 w-5 h-5 rounded-full
113 bg-[#ccc] border border-black/12
114 shadow-sm flex items-center justify-center"
115 >
116 <img
117 src={suggestion.favicon}
118 alt=""
119 class="w-4 h-4 filter brightness-105 contrast-103"
120 loading="lazy"
121 onerror={handleFaviconError}
122 style="image-rendering: -webkit-optimize-contrast;"
123 />
124 </div>
125 {/if}
126
127 <!-- URL text with gradient fade -->
128 <div
129 class="flex-1 min-w-0 flex gap-2 items-center
130 whitespace-nowrap overflow-hidden
131 [mask-image:linear-gradient(90deg,black_85%,transparent_100%)]
132 [-webkit-mask-image:linear-gradient(90deg,black_85%,transparent_100%)]"
133 >
134 <!-- Domain -->
135 <span class="text-[#e6e6e6] opacity-95">
136 {domain}
137 </span>
138
139 <!-- Separator -->
140 <span class="text-[#777]">
141 {' | '}
142 </span>
143
144 <!-- Path -->
145 <span class="text-[#99aadd]">
146 {path || '/'}
147 </span>
148 </div>
149 </div>
150 {/each}
151 </div>
152{/if}
153