Skip to content

Commit bb4b5a5

Browse files
committed
Add search focus highlight, Ctrl+F shortcut, and dynamic examples indexing
1 parent 02faf72 commit bb4b5a5

3 files changed

Lines changed: 75 additions & 14 deletions

File tree

src/lib/components/layout/Sidebar.svelte

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script lang="ts">
2+
import { onMount, onDestroy } from 'svelte';
23
import { base } from '$app/paths';
34
import { page } from '$app/stores';
45
import { goto } from '$app/navigation';
@@ -21,6 +22,22 @@
2122
let searchQuery = $state('');
2223
let searchResults = $derived(search(searchQuery, 15));
2324
let showResults = $derived(searchQuery.length > 0);
25+
let searchInput = $state<HTMLInputElement | null>(null);
26+
27+
function handleGlobalKeydown(event: KeyboardEvent) {
28+
if ((event.ctrlKey || event.metaKey) && event.key === 'f') {
29+
event.preventDefault();
30+
searchInput?.focus();
31+
}
32+
}
33+
34+
onMount(() => {
35+
window.addEventListener('keydown', handleGlobalKeydown);
36+
});
37+
38+
onDestroy(() => {
39+
window.removeEventListener('keydown', handleGlobalKeydown);
40+
});
2441
2542
// Check if we're on an API page or examples listing page
2643
// API pages can be /api, /api/latest, or /api/v0.16
@@ -32,8 +49,12 @@
3249
}
3350
3451
function handleSearchKeydown(event: KeyboardEvent) {
35-
if (event.key === 'Escape' && searchQuery) {
36-
searchQuery = '';
52+
if (event.key === 'Escape') {
53+
if (searchQuery) {
54+
searchQuery = '';
55+
} else {
56+
searchInput?.blur();
57+
}
3758
event.stopPropagation();
3859
}
3960
}
@@ -55,6 +76,7 @@
5576
5677
function getTypeIcon(type: SearchResult['type']): string {
5778
switch (type) {
79+
case 'page': return 'file';
5880
case 'module': return 'package';
5981
case 'class': return 'box';
6082
case 'function': return 'zap';
@@ -72,6 +94,7 @@
7294
type="text"
7395
placeholder="Search docs..."
7496
bind:value={searchQuery}
97+
bind:this={searchInput}
7598
class="search-input"
7699
onkeydown={handleSearchKeydown}
77100
/>
@@ -151,6 +174,11 @@
151174
height: var(--header-height);
152175
padding: 0 var(--space-md);
153176
border-bottom: 1px solid var(--border);
177+
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
178+
}
179+
180+
.search-container:focus-within {
181+
box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent) 25%, transparent);
154182
}
155183
156184
.search-icon {

src/lib/utils/search.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ let searchIndex: SearchIndex = { results: [], built: false };
2626
// Cache for loaded manifests
2727
let manifestsLoaded = false;
2828
let exampleResults: SearchResult[] = [];
29+
let packagesWithExamples: Set<string> = new Set();
2930

3031
/**
3132
* Build page-level search results for navigation pages
@@ -58,16 +59,18 @@ function buildPageResults(): SearchResult[] {
5859
tags: ['api', 'reference', 'documentation']
5960
});
6061

61-
// Examples page
62-
results.push({
63-
type: 'page',
64-
name: `${pkg.name} Examples`,
65-
description: `Example notebooks for ${pkg.name}`,
66-
path: `${packageId}/examples`,
67-
packageId,
68-
moduleName: '',
69-
tags: ['examples', 'tutorials', 'notebooks']
70-
});
62+
// Examples page (only if package has examples)
63+
if (packagesWithExamples.has(packageId)) {
64+
results.push({
65+
type: 'page',
66+
name: `${pkg.name} Examples`,
67+
description: `Example notebooks for ${pkg.name}`,
68+
path: `${packageId}/examples`,
69+
packageId,
70+
moduleName: '',
71+
tags: ['examples', 'tutorials', 'notebooks']
72+
});
73+
}
7174
}
7275

7376
return results;
@@ -157,6 +160,10 @@ async function loadExampleManifests(): Promise<SearchResult[]> {
157160

158161
const manifest: NotebookManifest = await response.json();
159162

163+
if (manifest.notebooks.length > 0) {
164+
packagesWithExamples.add(packageId);
165+
}
166+
160167
for (const notebook of manifest.notebooks) {
161168
results.push({
162169
type: 'example',
@@ -175,6 +182,10 @@ async function loadExampleManifests(): Promise<SearchResult[]> {
175182

176183
exampleResults = results;
177184
manifestsLoaded = true;
185+
186+
// Rebuild search index now that we know which packages have examples
187+
searchIndex.built = false;
188+
178189
return results;
179190
}
180191

src/routes/+page.svelte

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script lang="ts">
2+
import { onMount, onDestroy } from 'svelte';
23
import { base } from '$app/paths';
34
import { goto } from '$app/navigation';
45
import Icon from '$lib/components/common/Icon.svelte';
@@ -10,10 +11,30 @@
1011
let searchQuery = $state('');
1112
let searchResults = $derived(search(searchQuery, 8));
1213
let showResults = $derived(searchQuery.length > 0);
14+
let searchInput = $state<HTMLInputElement | null>(null);
15+
16+
function handleGlobalKeydown(event: KeyboardEvent) {
17+
if ((event.ctrlKey || event.metaKey) && event.key === 'f') {
18+
event.preventDefault();
19+
searchInput?.focus();
20+
}
21+
}
22+
23+
onMount(() => {
24+
window.addEventListener('keydown', handleGlobalKeydown);
25+
});
26+
27+
onDestroy(() => {
28+
window.removeEventListener('keydown', handleGlobalKeydown);
29+
});
1330
1431
function handleSearchKeydown(event: KeyboardEvent) {
15-
if (event.key === 'Escape' && searchQuery) {
16-
searchQuery = '';
32+
if (event.key === 'Escape') {
33+
if (searchQuery) {
34+
searchQuery = '';
35+
} else {
36+
searchInput?.blur();
37+
}
1738
event.stopPropagation();
1839
}
1940
}
@@ -62,6 +83,7 @@
6283
type="text"
6384
placeholder="Search the API..."
6485
bind:value={searchQuery}
86+
bind:this={searchInput}
6587
onkeydown={handleSearchKeydown}
6688
/>
6789
{#if searchQuery}

0 commit comments

Comments
 (0)