feat(omnibox): add Ctrl+Number quick navigation
Implements keyboard shortcuts for instant navigation to the first 10 results: - Ctrl+1-9: Navigate to results 1-9 - Ctrl+0: Navigate to 10th result - Works in both History and Favorites views - Uses event.code for keyboard layout independence (QWERTY, AZERTY, QWERTZ) - Visual number hints appear on the right side of each item - Hints highlighted with accent color on selection/hover
- commit
- a39a329e34d5c3fac458c1080e979f56c645813f
- author
- bnema · · 2025-11-16 18:10 UTC
- committer
- bnema · · 2025-11-16 20:27 UTC
- parent
- c99d69f
- signature
- unsigned
2 files · +71 -0
M gui/src/components/omnibox/OmniboxInput.svelte +39 -0
| @@ -185,6 +185,27 @@ | |||
| 185 | 185 | break; | |
| 186 | 186 | ||
| 187 | 187 | default: | |
| 188 | + | // Handle Ctrl+1 through Ctrl+0 for quick navigation to first 10 results | |
| 189 | + | // Use event.code for keyboard layout independence (QWERTY, AZERTY, QWERTZ, etc.) | |
| 190 | + | if (event.ctrlKey && mode === 'omnibox') { | |
| 191 | + | const code = event.code; | |
| 192 | + | let targetIndex = -1; | |
| 193 | + | ||
| 194 | + | // Map physical keys: Digit1->0, Digit2->1, ..., Digit9->8, Digit0->9 | |
| 195 | + | if (code >= 'Digit1' && code <= 'Digit9') { | |
| 196 | + | targetIndex = parseInt(code.substring(5), 10) - 1; | |
| 197 | + | } else if (code === 'Digit0') { | |
| 198 | + | targetIndex = 9; | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | if (targetIndex >= 0) { | |
| 202 | + | event.preventDefault(); | |
| 203 | + | event.stopPropagation(); | |
| 204 | + | handleNumberKey(targetIndex); | |
| 205 | + | return; | |
| 206 | + | } | |
| 207 | + | } | |
| 208 | + | ||
| 188 | 209 | // For normal typing keys, only stop propagation to prevent page handlers | |
| 189 | 210 | // but don't prevent default so typing still works in the input | |
| 190 | 211 | event.stopPropagation(); | |
| @@ -245,6 +266,24 @@ | |||
| 245 | 266 | }, 100); | |
| 246 | 267 | } | |
| 247 | 268 | ||
| 269 | + | // Handle Ctrl+Number key for quick navigation | |
| 270 | + | function handleNumberKey(targetIndex: number) { | |
| 271 | + | if (mode !== 'omnibox') return; | |
| 272 | + | ||
| 273 | + | // Get the current list based on view mode | |
| 274 | + | const currentList = viewMode === 'history' ? suggestions : favorites; | |
| 275 | + | ||
| 276 | + | // Check if the target index is valid | |
| 277 | + | if (targetIndex >= currentList.length) return; | |
| 278 | + | ||
| 279 | + | const item = currentList[targetIndex]; | |
| 280 | + | if (!item || !item.url) return; | |
| 281 | + | ||
| 282 | + | // Navigate to the item and close omnibox | |
| 283 | + | omniboxBridge.navigate(item.url); | |
| 284 | + | omniboxStore.close(); | |
| 285 | + | } | |
| 286 | + | ||
| 248 | 287 | // Handle arrow key navigation | |
| 249 | 288 | function handleArrowKeys(key: string) { | |
| 250 | 289 | let totalItems: number; | |
| @@ -185,6 +185,27 @@ | |||||
| 185 | break; | 185 | break; | ||
| 186 | 186 | ||||
| 187 | default: | 187 | default: | ||
| 188 | + | // Handle Ctrl+1 through Ctrl+0 for quick navigation to first 10 results | |||
| 189 | + | // Use event.code for keyboard layout independence (QWERTY, AZERTY, QWERTZ, etc.) | |||
| 190 | + | if (event.ctrlKey && mode === 'omnibox') { | |||
| 191 | + | const code = event.code; | |||
| 192 | + | let targetIndex = -1; | |||
| 193 | + | ||||
| 194 | + | // Map physical keys: Digit1->0, Digit2->1, ..., Digit9->8, Digit0->9 | |||
| 195 | + | if (code >= 'Digit1' && code <= 'Digit9') { | |||
| 196 | + | targetIndex = parseInt(code.substring(5), 10) - 1; | |||
| 197 | + | } else if (code === 'Digit0') { | |||
| 198 | + | targetIndex = 9; | |||
| 199 | + | } | |||
| 200 | + | ||||
| 201 | + | if (targetIndex >= 0) { | |||
| 202 | + | event.preventDefault(); | |||
| 203 | + | event.stopPropagation(); | |||
| 204 | + | handleNumberKey(targetIndex); | |||
| 205 | + | return; | |||
| 206 | + | } | |||
| 207 | + | } | |||
| 208 | + | ||||
| 188 | // For normal typing keys, only stop propagation to prevent page handlers | 209 | // For normal typing keys, only stop propagation to prevent page handlers | ||
| 189 | // but don't prevent default so typing still works in the input | 210 | // but don't prevent default so typing still works in the input | ||
| 190 | event.stopPropagation(); | 211 | event.stopPropagation(); | ||
| @@ -245,6 +266,24 @@ | |||||
| 245 | }, 100); | 266 | }, 100); | ||
| 246 | } | 267 | } | ||
| 247 | 268 | ||||
| 269 | + | // Handle Ctrl+Number key for quick navigation | |||
| 270 | + | function handleNumberKey(targetIndex: number) { | |||
| 271 | + | if (mode !== 'omnibox') return; | |||
| 272 | + | ||||
| 273 | + | // Get the current list based on view mode | |||
| 274 | + | const currentList = viewMode === 'history' ? suggestions : favorites; | |||
| 275 | + | ||||
| 276 | + | // Check if the target index is valid | |||
| 277 | + | if (targetIndex >= currentList.length) return; | |||
| 278 | + | ||||
| 279 | + | const item = currentList[targetIndex]; | |||
| 280 | + | if (!item || !item.url) return; | |||
| 281 | + | ||||
| 282 | + | // Navigate to the item and close omnibox | |||
| 283 | + | omniboxBridge.navigate(item.url); | |||
| 284 | + | omniboxStore.close(); | |||
| 285 | + | } | |||
| 286 | + | ||||
| 248 | // Handle arrow key navigation | 287 | // Handle arrow key navigation | ||
| 249 | function handleArrowKeys(key: string) { | 288 | function handleArrowKeys(key: string) { | ||
| 250 | let totalItems: number; | 289 | let totalItems: number; | ||
M gui/src/components/omnibox/OmniboxSuggestions.svelte +32 -0
| @@ -152,6 +152,7 @@ | |||
| 152 | 152 | {@const isSelected = index === selectedIndex} | |
| 153 | 153 | {@const favicon = item.favicon || item.favicon_url || ''} | |
| 154 | 154 | {@const isFav = viewMode === 'history' && isFavorited(item.url)} | |
| 155 | + | {@const shortcutNumber = index < 9 ? index + 1 : index === 9 ? 0 : null} | |
| 155 | 156 | ||
| 156 | 157 | <div | |
| 157 | 158 | id="omnibox-item-{index}" | |
| @@ -195,6 +196,13 @@ | |||
| 195 | 196 | {path || '/'} | |
| 196 | 197 | </span> | |
| 197 | 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} | |
| 198 | 206 | </div> | |
| 199 | 207 | {/each} | |
| 200 | 208 | {/if} | |
| @@ -361,4 +369,28 @@ | |||
| 361 | 369 | mask-image: linear-gradient(90deg, black 85%, transparent 100%); | |
| 362 | 370 | -webkit-mask-image: linear-gradient(90deg, black 85%, transparent 100%); | |
| 363 | 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 | + | } | |
| 364 | 396 | </style> | |
| @@ -152,6 +152,7 @@ | |||||
| 152 | {@const isSelected = index === selectedIndex} | 152 | {@const isSelected = index === selectedIndex} | ||
| 153 | {@const favicon = item.favicon || item.favicon_url || ''} | 153 | {@const favicon = item.favicon || item.favicon_url || ''} | ||
| 154 | {@const isFav = viewMode === 'history' && isFavorited(item.url)} | 154 | {@const isFav = viewMode === 'history' && isFavorited(item.url)} | ||
| 155 | + | {@const shortcutNumber = index < 9 ? index + 1 : index === 9 ? 0 : null} | |||
| 155 | 156 | ||||
| 156 | <div | 157 | <div | ||
| 157 | id="omnibox-item-{index}" | 158 | id="omnibox-item-{index}" | ||
| @@ -195,6 +196,13 @@ | |||||
| 195 | {path || '/'} | 196 | {path || '/'} | ||
| 196 | </span> | 197 | </span> | ||
| 197 | </div> | 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} | |||
| 198 | </div> | 206 | </div> | ||
| 199 | {/each} | 207 | {/each} | ||
| 200 | {/if} | 208 | {/if} | ||
| @@ -361,4 +369,28 @@ | |||||
| 361 | mask-image: linear-gradient(90deg, black 85%, transparent 100%); | 369 | mask-image: linear-gradient(90deg, black 85%, transparent 100%); | ||
| 362 | -webkit-mask-image: linear-gradient(90deg, black 85%, transparent 100%); | 370 | -webkit-mask-image: linear-gradient(90deg, black 85%, transparent 100%); | ||
| 363 | } | 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 | + | } | |||
| 364 | </style> | 396 | </style> | ||