v0.10.0 0f4e9eb

OmniboxInput.svelte RawHistoryFolder

1<!--
2  Omnibox Input Component
3
4  Input field with keyboard handling and debounced search
5-->
6<script lang="ts">
7  import { omniboxStore } from './stores.svelte.ts';
8  import { debouncedQuery, omniboxBridge } from './messaging';
9  import { findInPage } from './find';
10
11  // Reactive state
12  let mode = $derived(omniboxStore.mode);
13  let inputValue = $derived(omniboxStore.inputValue);
14  let selectedIndex = $derived(omniboxStore.selectedIndex);
15  let suggestions = $derived(omniboxStore.suggestions);
16  let matches = $derived(omniboxStore.matches);
17
18  // Input element ref and responsive styles (props for parent to bind)
19  interface Props {
20    inputElement?: HTMLInputElement;
21    responsiveStyles?: {
22      fontSize: string;
23      inputPadding: string;
24    };
25  }
26
27  let { inputElement = $bindable(), responsiveStyles }: Props = $props();
28
29  const ACCENT_FALLBACK = 'var(--dynamic-accent)';
30
31  function resolveAccentColor(): string {
32    if (!inputElement) return ACCENT_FALLBACK;
33    const inlineAccent = inputElement.style.getPropertyValue('--dumber-input-accent');
34    if (inlineAccent && inlineAccent.trim()) {
35      return inlineAccent.trim();
36    }
37    try {
38      const computedAccent = getComputedStyle(inputElement).getPropertyValue('--dynamic-accent');
39      if (computedAccent && computedAccent.trim()) {
40        return computedAccent.trim();
41      }
42    } catch { /* no-op */ }
43    return ACCENT_FALLBACK;
44  }
45
46  // Computed placeholder
47  let placeholder = $derived(
48    mode === 'find' ? 'Find in page…' : 'Type URL or search…'
49  );
50
51  // Handle input changes
52  function handleInput(event: Event) {
53    const target = event.target as HTMLInputElement;
54    const value = target.value || '';
55
56    omniboxStore.setInputValue(value);
57    omniboxStore.setFaded(false);
58
59    if (mode === 'omnibox') {
60      // Debounced search for omnibox
61      debouncedQuery(value);
62    } else if (mode === 'find') {
63      // Immediate find for search
64      findInPage(value);
65    }
66  }
67
68  // Handle key events
69  function handleKeyDown(event: KeyboardEvent) {
70    switch (event.key) {
71      case 'Escape':
72        event.preventDefault();
73        event.stopPropagation();
74        omniboxStore.close();
75        break;
76
77      case 'Enter':
78        event.preventDefault();
79        event.stopPropagation();
80        handleEnterKey(event);
81        break;
82
83      case 'ArrowDown':
84      case 'ArrowUp':
85        event.preventDefault();
86        event.stopPropagation();
87        handleArrowKeys(event.key);
88        break;
89
90      default:
91        // For normal typing keys, only stop propagation to prevent page handlers
92        // but don't prevent default so typing still works in the input
93        event.stopPropagation();
94        // Any other key should restore full opacity
95        omniboxStore.setFaded(false);
96        break;
97    }
98  }
99
100  // Handle Enter key based on mode
101  function handleEnterKey(event: KeyboardEvent) {
102    if (mode === 'omnibox') {
103      const selectedSuggestion = selectedIndex >= 0 ? suggestions[selectedIndex] : null;
104      const url = selectedSuggestion?.url || inputValue || '';
105
106      if (url) {
107        omniboxBridge.navigate(url);
108        omniboxStore.close();
109      }
110    } else if (mode === 'find') {
111      if (event.shiftKey) {
112        // Shift+Enter: jump to previous match
113        jumpToMatch(-1);
114      } else if (event.altKey) {
115        // Alt+Enter: center on current match but keep overlay open
116        revealCurrentMatch();
117      } else {
118        // Enter: center and close
119        revealCurrentMatch();
120        omniboxStore.close();
121      }
122    }
123  }
124
125  // Handle arrow key navigation
126  function handleArrowKeys(key: string) {
127    const totalItems = mode === 'omnibox' ? suggestions.length : matches.length;
128
129    if (totalItems > 0) {
130      if (key === 'ArrowDown') {
131        omniboxStore.selectNext();
132      } else {
133        omniboxStore.selectPrevious();
134      }
135
136      // Fade overlay while navigating results
137      omniboxStore.setFaded(true);
138
139      // For find mode, reveal the current match
140      if (mode === 'find') {
141        revealCurrentMatch();
142      }
143    }
144  }
145
146  // Jump to match (for find mode)
147  function jumpToMatch(delta: number) {
148    if (mode !== 'find' || matches.length === 0) return;
149
150    const currentIndex = selectedIndex || 0;
151    const newIndex = ((currentIndex + delta) % matches.length + matches.length) % matches.length;
152
153    omniboxStore.setSelectedIndex(newIndex);
154    revealCurrentMatch();
155  }
156
157  // Reveal current match (for find mode)
158  function revealCurrentMatch() {
159    if (mode !== 'find' || selectedIndex < 0 || !matches[selectedIndex]) return;
160
161    // Remove previous active class
162    const prevMatch = matches[omniboxStore.activeIndex];
163    if (prevMatch?.element?.classList) {
164      prevMatch.element.classList.remove('find-active');
165    }
166
167    // Add active class to current match
168    const currentMatch = matches[selectedIndex];
169    if (currentMatch?.element?.classList) {
170      currentMatch.element.classList.add('find-active');
171      omniboxStore.setActiveIndex(selectedIndex);
172
173      // Scroll into view
174      try {
175        currentMatch.element.scrollIntoView({
176          block: 'center',
177          inline: 'nearest'
178        });
179      } catch {
180        currentMatch.element.scrollIntoView();
181      }
182    }
183  }
184
185  // Handle mouse events
186  function handleMouseDown() {
187    omniboxStore.setFaded(false);
188  }
189
190  function handleClick() {
191    omniboxStore.setFaded(false);
192  }
193
194  function handleFocus() {
195    omniboxStore.setFaded(false);
196    if (inputElement) {
197      const accent = resolveAccentColor();
198      inputElement.style.setProperty('--dumber-input-border-color', accent);
199    }
200  }
201
202  function handleBlur() {
203    if (!inputElement) return;
204    const base = inputElement.style.getPropertyValue('--dumber-input-border-base');
205    const fallback = (base && base.trim()) || 'var(--dynamic-border)';
206    inputElement.style.setProperty('--dumber-input-border-color', fallback);
207  }
208
209  function handleMouseEnter() {
210    if (omniboxStore.visible && inputElement) {
211      try {
212        inputElement.focus({ preventScroll: true });
213        inputElement.select();
214      } catch {
215        inputElement.focus();
216      }
217    }
218  }
219</script>
220
221<input
222  bind:this={inputElement}
223  type="text"
224  {placeholder}
225  value={inputValue}
226  class="w-full box-border omnibox-input omnibox-input-field rounded focus:outline-none"
227  style="padding: {responsiveStyles?.inputPadding || '10px 12px'};
228         font-size: {responsiveStyles?.fontSize || '16px'};
229         box-sizing: border-box;"
230  oninput={handleInput}
231  onkeydown={handleKeyDown}
232  onmousedown={handleMouseDown}
233  onclick={handleClick}
234  onfocus={handleFocus}
235  onblur={handleBlur}
236  onmouseenter={handleMouseEnter}
237  autocomplete="off"
238  spellcheck="false"
239  role="combobox"
240  aria-expanded={omniboxStore.hasContent}
241  aria-controls="omnibox-list"
242  aria-haspopup="listbox"
243  aria-owns="omnibox-list"
244  aria-activedescendant={selectedIndex >= 0 ? `omnibox-item-${selectedIndex}` : undefined}
245/>
246
247<style>
248  .omnibox-input-field {
249    background: var(--dumber-input-bg, var(--dynamic-bg));
250    color: var(--dynamic-text);
251    border: 2px solid var(--dumber-input-border-color, var(--dynamic-border));
252    transition: border-color 120ms ease, background-color 120ms ease, color 120ms ease;
253  }
254
255  .omnibox-input-field::placeholder {
256    color: var(--dynamic-muted);
257  }
258
259  .omnibox-input-field:focus {
260    color: var(--dynamic-text);
261  }
262</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