v0.14.0 76d1892

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 favorites = $derived(omniboxStore.favorites);
13  let viewMode = $derived(omniboxStore.viewMode);
14  let selectedIndex = $derived(omniboxStore.selectedIndex);
15  let inputValue = $derived(omniboxStore.inputValue);
16  let hasContent = $state(false);
17
18  // Debug effect to log favorites changes
19  $effect(() => {
20    console.log('[OmniboxSuggestions] Favorites updated, count:', favorites.length);
21  });
22
23  // Computed: Get current list based on view mode with local filtering for favorites
24  let currentList = $derived.by(() => {
25    const list = viewMode === 'history' ? suggestions : favorites;
26
27    // In favorites view with input, filter locally
28    if (viewMode === 'favorites' && inputValue && inputValue.trim()) {
29      const query = inputValue.toLowerCase().trim();
30      return list.filter(fav =>
31        fav.url.toLowerCase().includes(query) ||
32        (fav.title && fav.title.toLowerCase().includes(query))
33      );
34    }
35
36    return list;
37  });
38
39  // Update hasContent - always show in omnibox mode to display header
40  $effect(() => {
41    hasContent = omniboxStore.mode === 'omnibox';
42  });
43
44  // Check if a URL is in favorites
45  function isFavorited(url: string): boolean {
46    return favorites.some(fav => fav.url === url);
47  }
48
49  // Handle suggestion item mouse enter
50  function handleItemMouseEnter(index: number) {
51    omniboxStore.setSelectedIndex(index);
52    omniboxStore.setFaded(false);
53    scrollToSelection();
54  }
55
56  // Handle suggestion item click
57  function handleItemClick(suggestion: any) {
58    omniboxBridge.navigate(suggestion.url);
59    omniboxStore.close();
60  }
61
62  // Handle keyboard activation on suggestion item
63  function handleItemKeyDown(event: KeyboardEvent, suggestion: any) {
64    if (event.key === 'Enter' || event.key === ' ') {
65      event.preventDefault();
66      handleItemClick(suggestion);
67    }
68  }
69
70  // Scroll list to show selected item
71  function scrollToSelection() {
72    const selectedItem = document.getElementById(`omnibox-item-${selectedIndex}`);
73    if (selectedItem && selectedItem.scrollIntoView) {
74      try {
75        selectedItem.scrollIntoView({ block: 'nearest' });
76      } catch {
77        selectedItem.scrollIntoView();
78      }
79    }
80  }
81
82  // Parse URL for display
83  function parseUrl(url: string) {
84    try {
85      const urlObj = new URL(url, window.location.href);
86      return {
87        domain: urlObj.hostname,
88        path: (urlObj.pathname || '') + (urlObj.search || '') + (urlObj.hash || '')
89      };
90    } catch {
91      return {
92        domain: url || '',
93        path: ''
94      };
95    }
96  }
97
98  // Handle favicon error
99  function handleFaviconError(event: Event) {
100    const target = event.target as HTMLImageElement;
101    const chip = target.parentElement;
102    if (chip) {
103      chip.style.display = 'none';
104    }
105  }
106
107  // Watch for selection changes to scroll
108  $effect(() => {
109    if (selectedIndex >= 0) {
110      scrollToSelection();
111    }
112  });
113</script>
114
115{#if hasContent}
116  <div
117    id="omnibox-list"
118    class="suggestion-list"
119    role="listbox"
120    aria-label="Search suggestions"
121  >
122    <!-- View mode indicator -->
123    <div class="view-mode-header">
124      <span class={viewMode === 'history' ? 'view-tab active' : 'view-tab'}>
125        History ({suggestions.length})
126      </span>
127      <span class={viewMode === 'favorites' ? 'view-tab active' : 'view-tab'}>
128        Favorites ({favorites.length})
129      </span>
130      <span class="view-hint">Tab to switch</span>
131    </div>
132
133    {#if currentList.length === 0}
134      <!-- Empty state -->
135      {#if viewMode === 'favorites'}
136        <!-- Always show empty state for favorites -->
137        <div class="empty-state">
138          <span class="empty-icon">⭐</span>
139          <span class="empty-text">No favorites yet</span>
140          <span class="empty-hint">Press Space on any item in History to add it here</span>
141        </div>
142      {:else if inputValue && inputValue.trim()}
143        <!-- Only show "No results found" for history when user has typed something -->
144        <div class="empty-state">
145          <span class="empty-icon">🔍</span>
146          <span class="empty-text">No results found</span>
147        </div>
148      {/if}
149    {:else}
150      {#each currentList as item, index (item.url)}
151        {@const { domain, path } = parseUrl(item.url)}
152        {@const isSelected = index === selectedIndex}
153        {@const favicon = item.favicon || item.favicon_url || ''}
154        {@const isFav = viewMode === 'history' && isFavorited(item.url)}
155        {@const shortcutNumber = index < 9 ? index + 1 : index === 9 ? 0 : null}
156
157        <div
158          id="omnibox-item-{index}"
159          class:suggestion-item={true}
160          class:selected={isSelected}
161          class:favorited={isFav}
162          role="option"
163          tabindex={isSelected ? 0 : -1}
164          aria-selected={isSelected}
165          onmouseenter={() => handleItemMouseEnter(index)}
166          onclick={() => handleItemClick(item)}
167          onkeydown={(e) => handleItemKeyDown(e, item)}
168        >
169        <!-- Favicon chip -->
170        {#if favicon}
171          <div class="suggestion-favicon">
172            <img
173              src={favicon}
174              alt=""
175              class="suggestion-favicon-img"
176              loading="lazy"
177              onerror={handleFaviconError}
178            />
179          </div>
180        {/if}
181
182        <!-- URL text with gradient fade -->
183        <div class="suggestion-text">
184          <!-- Domain -->
185          <span class="suggestion-domain">
186            {domain}
187          </span>
188
189          <!-- Separator -->
190          <span class="suggestion-separator">
191            {' | '}
192          </span>
193
194          <!-- Path -->
195          <span class="suggestion-path">
196            {path || '/'}
197          </span>
198        </div>
199
200        <!-- Keyboard shortcut hint (Ctrl+1-9, Ctrl+0 for 10th) -->
201        {#if shortcutNumber !== null}
202          <div class="suggestion-shortcut-hint" aria-hidden="true">
203            {shortcutNumber}
204          </div>
205        {/if}
206      </div>
207    {/each}
208    {/if}
209  </div>
210{/if}
211
212
213
214<style>
215  .suggestion-list {
216    margin-top: 0.5rem;
217    max-height: 50vh;
218    overflow-y: auto;
219    border-top: 1px solid var(--dynamic-border);
220    background: var(--dynamic-bg);
221    border-radius: 0 0 2px 2px;
222  }
223
224  .view-mode-header {
225    display: flex;
226    gap: 0.5rem;
227    align-items: center;
228    padding: 0.5rem 0.85rem;
229    border-bottom: 1px solid var(--dynamic-border);
230    background: color-mix(in srgb, var(--dynamic-bg) 95%, var(--dynamic-text));
231    font-family: 'Fira Sans', system-ui, -apple-system, 'Segoe UI', 'Ubuntu', 'Cantarell', sans-serif;
232  }
233
234  .view-tab {
235    padding: 0.25rem 0.75rem;
236    border-radius: 2px;
237    font-size: 0.85rem;
238    font-weight: 500;
239    color: var(--dynamic-muted);
240    transition: all 100ms ease;
241  }
242
243  .view-tab.active {
244    color: var(--dynamic-accent);
245    background: color-mix(in srgb, var(--dynamic-accent) 15%, transparent);
246  }
247
248  .view-hint {
249    margin-left: auto;
250    font-size: 0.75rem;
251    color: var(--dynamic-muted);
252    opacity: 0.7;
253  }
254
255  .empty-state {
256    display: flex;
257    flex-direction: column;
258    align-items: center;
259    justify-content: center;
260    padding: 2rem 1rem;
261    gap: 0.5rem;
262    color: var(--dynamic-muted);
263    text-align: center;
264  }
265
266  .empty-icon {
267    font-size: 2rem;
268    opacity: 0.5;
269  }
270
271  .empty-text {
272    font-size: 0.95rem;
273    font-weight: 500;
274    color: var(--dynamic-text);
275    opacity: 0.7;
276  }
277
278  .empty-hint {
279    font-size: 0.8rem;
280    opacity: 0.6;
281    max-width: 300px;
282  }
283
284  .suggestion-item {
285    display: flex;
286    gap: 0.75rem;
287    align-items: center;
288    padding: 0.85rem 0.85rem;
289    border-bottom: 1px solid color-mix(in srgb, var(--dynamic-border) 50%, transparent);
290    cursor: pointer;
291    transition: background-color 100ms ease, border-left-color 100ms ease;
292    letter-spacing: normal;
293    border-left: 3px solid transparent;
294    position: relative;
295    font-family: 'Fira Sans', system-ui, -apple-system, 'Segoe UI', 'Ubuntu', 'Cantarell', sans-serif;
296  }
297
298  .suggestion-item:last-child {
299    border-bottom: none;
300  }
301
302  /* Favorited items get a permanent yellow left border */
303  .suggestion-item.favorited {
304    border-left-color: #f59e0b;
305  }
306
307  .suggestion-item.selected,
308  .suggestion-item:hover,
309  .suggestion-item:focus-visible {
310    background: color-mix(in srgb, var(--dynamic-accent) 15%, var(--dynamic-bg));
311    border-left-color: var(--dynamic-accent);
312    color: var(--dynamic-text);
313    outline: none;
314  }
315
316  /* Keep yellow border even when selected/hover for favorited items */
317  .suggestion-item.favorited.selected,
318  .suggestion-item.favorited:hover,
319  .suggestion-item.favorited:focus-visible {
320    border-left-color: #f59e0b;
321  }
322
323  .suggestion-item.selected {
324    font-weight: 500;
325  }
326
327  .suggestion-item .suggestion-domain {
328    color: var(--dynamic-text);
329    opacity: 0.95;
330  }
331
332  .suggestion-item .suggestion-separator {
333    color: var(--dynamic-muted);
334  }
335
336  .suggestion-item .suggestion-path {
337    color: var(--dynamic-muted);
338  }
339
340  .suggestion-favicon {
341    width: 24px;
342    height: 24px;
343    border: none;
344    border-radius: 3px;
345    display: flex;
346    align-items: center;
347    justify-content: center;
348    background: transparent;
349    flex-shrink: 0;
350    overflow: hidden;
351  }
352
353  .suggestion-favicon-img {
354    width: 24px;
355    height: 24px;
356    object-fit: contain;
357    image-rendering: auto;
358  }
359
360  .suggestion-text {
361    flex: 1;
362    min-width: 0;
363    display: flex;
364    gap: 0.75rem;
365    align-items: center;
366    white-space: nowrap;
367    overflow: hidden;
368    text-overflow: ellipsis;
369    mask-image: linear-gradient(90deg, black 85%, transparent 100%);
370    -webkit-mask-image: linear-gradient(90deg, black 85%, transparent 100%);
371  }
372
373  .suggestion-shortcut-hint {
374    position: absolute;
375    right: 0.85rem;
376    top: 50%;
377    transform: translateY(-50%);
378    font-size: 0.75rem;
379    font-weight: 600;
380    color: var(--dynamic-muted);
381    opacity: 0.6;
382    background: var(--dynamic-bg);
383    padding: 0.25rem 0.5rem;
384    border-radius: 3px;
385    border: 1px solid var(--dynamic-border);
386    z-index: 10;
387    pointer-events: none;
388    transition: opacity 100ms ease, color 100ms ease;
389  }
390
391  .suggestion-item.selected .suggestion-shortcut-hint,
392  .suggestion-item:hover .suggestion-shortcut-hint {
393    opacity: 0.9;
394    color: var(--dynamic-accent);
395  }
396</style>

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close