v0.11.0 dc50343

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>

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