Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 55 additions & 41 deletions docs/src/components/SearchModal.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<script lang="ts">
import { onMount } from 'svelte';
import DOMPurify from 'dompurify';

interface WindowWithPagefind {
pagefind?: {
search: (query: string) => Promise<PagefindSearchResponse>;
init: () => Promise<void>;
};
attributeIndex?: AttributeIndex[];
}

interface PagefindResult {
id: string;
data: () => Promise<{
Expand Down Expand Up @@ -40,19 +41,21 @@ interface AttributeIndex {
deprecated: boolean;
}

let isOpen = false;
let query = '';
let attributeResults: AttributeIndex[] = [];
let pageResults: SearchResult[] = [];
let selectedIndex = 0;
let isLoading = false;
let inputEl: HTMLInputElement;
let resultsEl: HTMLDivElement;
let usingKeyboard = false;

$: totalResults = attributeResults.length + pageResults.length;
$: hasResults = attributeResults.length > 0 || pageResults.length > 0;
$: noResults = query && !isLoading && !hasResults;
let isOpen = $state(false);
let query = $state('');
let attributeResults = $state<AttributeIndex[]>([]);
let pageResults = $state<SearchResult[]>([]);
let selectedIndex = $state(0);
let isLoading = $state(false);
// biome-ignore lint/style/useConst: <false flag by biome. We bind this state to an element and it needs to be mutable>
let inputEl: HTMLInputElement | undefined = $state();
// biome-ignore lint/style/useConst: <false flag by biome. We bind this state to an element and it needs to be mutable>
let resultsEl: HTMLDivElement | undefined = $state();
let usingKeyboard = $state(false);

const totalResults = $derived(attributeResults.length + pageResults.length);
const hasResults = $derived(attributeResults.length > 0 || pageResults.length > 0);
const noResults = $derived(query && !isLoading && !hasResults);

async function loadAttributeIndex() {
const windowWithPagefind = window as WindowWithPagefind;
Expand Down Expand Up @@ -102,7 +105,8 @@ function handleTriggerClick() {
isOpen = true;
}

onMount(() => {
// Setup global event listeners
$effect(() => {
document.addEventListener('keydown', handleGlobalKeyDown);
const trigger = document.getElementById('search-trigger');
trigger?.addEventListener('click', handleTriggerClick);
Expand All @@ -113,22 +117,28 @@ onMount(() => {
};
});

$: if (isOpen) {
loadAttributeIndex();
loadPagefind();
query = '';
attributeResults = [];
pageResults = [];
selectedIndex = 0;
setTimeout(() => inputEl?.focus(), 0);
}
// Handle modal open
$effect(() => {
if (isOpen) {
loadAttributeIndex();
loadPagefind();
query = '';
attributeResults = [];
pageResults = [];
selectedIndex = 0;
setTimeout(() => inputEl?.focus(), 0);
}
});

// Search effect
let searchTimeout: ReturnType<typeof setTimeout>;

$: {
$effect(() => {
const currentQuery = query;

clearTimeout(searchTimeout);
searchTimeout = setTimeout(async () => {
const trimmedQuery = query.trim().toLowerCase();
const trimmedQuery = currentQuery.trim().toLowerCase();

if (!trimmedQuery) {
attributeResults = [];
Expand Down Expand Up @@ -169,7 +179,7 @@ $: {
// Search pages with Pagefind (async)
if (windowWithPagefind.pagefind) {
try {
const response = await windowWithPagefind.pagefind.search(query);
const response = await windowWithPagefind.pagefind.search(currentQuery);
const searchResults = await Promise.all(
response.results.slice(0, 5).map(async (result) => {
const data = await result.data();
Expand All @@ -190,7 +200,7 @@ $: {

isLoading = false;
}, 100);
}
});

function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'ArrowDown') {
Expand Down Expand Up @@ -242,10 +252,13 @@ function navigateToResult(result: SearchResult) {
window.location.href = result.url;
}

$: if (resultsEl && selectedIndex >= 0) {
const selectedElement = resultsEl.querySelector('.selected') as HTMLElement;
selectedElement?.scrollIntoView({ block: 'nearest' });
}
// Scroll selected item into view
$effect(() => {
if (resultsEl && selectedIndex >= 0) {
const selectedElement = resultsEl.querySelector('.selected') as HTMLElement;
selectedElement?.scrollIntoView({ block: 'nearest' });
}
});

function highlightMatch(key: string, searchQuery: string): { before: string; match: string; after: string } | null {
const lowerKey = key.toLowerCase();
Expand All @@ -264,14 +277,15 @@ function highlightMatch(key: string, searchQuery: string): { before: string; mat

{#if isOpen}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="fixed inset-0 bg-black/70 backdrop-blur-sm z-[1000] flex items-start justify-center pt-[10vh]"
on:click={() => isOpen = false}
onclick={() => isOpen = false}
>
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div
class="w-full max-w-2xl bg-bg-secondary border border-border rounded-lg shadow-lg overflow-hidden mx-4"
on:click|stopPropagation
onclick={(e) => e.stopPropagation()}
>
<div class="flex items-center gap-3 p-4 border-b border-border">
<svg class="text-text-muted flex-shrink-0" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
Expand All @@ -284,7 +298,7 @@ function highlightMatch(key: string, searchQuery: string): { before: string; mat
class="flex-1 bg-transparent border-none outline-none text-lg text-text-primary font-sans placeholder:text-text-muted"
placeholder="Search attributes (e.g., sentry.op, http.)"
bind:value={query}
on:keydown={handleKeyDown}
onkeydown={handleKeyDown}
/>
<kbd class="px-2 py-1 bg-bg-elevated border border-border rounded-sm text-xs text-text-muted font-sans">ESC</kbd>
</div>
Expand All @@ -310,9 +324,9 @@ function highlightMatch(key: string, searchQuery: string): { before: string; mat
{@const highlighted = highlightMatch(attr.key, query)}
<button
class="flex flex-col gap-1 w-full px-4 py-3 bg-transparent border-none border-b border-border last:border-b-0 text-left cursor-pointer transition-all duration-fast border-l-2 {index === selectedIndex ? 'bg-accent/15 border-l-accent selected shadow-[inset_0_0_0_1px_rgba(149,128,255,0.2)]' : 'border-l-transparent hover:bg-bg-hover hover:border-l-border-light'}"
on:click={() => navigateToAttribute(attr)}
on:mouseenter={() => handleMouseEnter(index)}
on:mousemove={handleMouseMove}
onclick={() => navigateToAttribute(attr)}
onmouseenter={() => handleMouseEnter(index)}
onmousemove={handleMouseMove}
>
<div class="flex items-center justify-between gap-3 flex-wrap">
<code class="font-mono text-sm font-medium bg-transparent p-0 border-none text-accent">
Expand Down Expand Up @@ -346,9 +360,9 @@ function highlightMatch(key: string, searchQuery: string): { before: string; mat
{@const actualIndex = attributeResults.length + index}
<button
class="flex flex-col gap-1 w-full px-4 py-3 bg-transparent border-none border-b border-border last:border-b-0 text-left cursor-pointer transition-all duration-fast border-l-2 {actualIndex === selectedIndex ? 'bg-accent/15 border-l-accent selected shadow-[inset_0_0_0_1px_rgba(149,128,255,0.2)]' : 'border-l-transparent hover:bg-bg-hover hover:border-l-border-light'}"
on:click={() => navigateToResult(result)}
on:mouseenter={() => handleMouseEnter(actualIndex)}
on:mousemove={handleMouseMove}
onclick={() => navigateToResult(result)}
onmouseenter={() => handleMouseEnter(actualIndex)}
onmousemove={handleMouseMove}
>
<span class="text-sm font-medium {actualIndex === selectedIndex ? 'text-accent' : 'text-text-primary'}">
{result.title}
Expand Down
Loading