Skip to content
Merged
Show file tree
Hide file tree
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
22 changes: 11 additions & 11 deletions docs/src/components/Search.astro
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ const base = import.meta.env.BASE_URL;
<svg class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<span class="hidden sm:inline">Search docs…</span>
<span class="hidden sm:inline">Search docs…</span>
<kbd class="hidden sm:inline-flex ml-auto text-xs border border-surface-300 dark:border-surface-600 rounded px-1.5 py-0.5 text-surface-400" id="search-shortcut-kbd">
<span class="text-xs" id="search-modifier">⌘</span>K
<span class="text-xs" id="search-modifier">Γîÿ</span>K
</kbd>
Comment on lines +14 to 17

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several user-visible strings appear to have mojibake/encoding corruption (e.g., "Search docsΓǪ" and the modifier glyph "Γîÿ"). These should be the intended Unicode characters (ellipsis and ⌘) so the UI renders correctly across browsers.

Copilot uses AI. Check for mistakes.
</button>
</div>
Expand All @@ -30,7 +30,7 @@ const base = import.meta.env.BASE_URL;
<input
id="search-input"
type="text"
placeholder="Search documentation…"
placeholder="Search documentation…"
class="flex-1 py-3 bg-transparent text-surface-900 dark:text-white placeholder-surface-400 outline-none text-base"
autocomplete="off"
/>
Expand All @@ -39,7 +39,7 @@ const base = import.meta.env.BASE_URL;
<div id="search-status" class="hidden px-4 py-2 text-xs text-surface-500 dark:text-surface-400 border-b border-surface-100 dark:border-surface-800"></div>
<div id="search-results" class="max-h-[60vh] overflow-y-auto p-2">
<div class="p-8 text-center text-surface-400 text-sm" id="search-empty">
Start typing to search…
Start typing to search…
</div>
Comment on lines 33 to 43

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The search input placeholder / empty-state text contains mojibake ("…") instead of the intended ellipsis character. This will show incorrect characters in the docs UI; please restore the proper Unicode punctuation.

Copilot uses AI. Check for mistakes.
</div>

Expand All @@ -48,7 +48,7 @@ const base = import.meta.env.BASE_URL;
</div>

<script define:vars={{ base }}>
// Pagefind instance persists across View Transitions — no need to re-import
// Pagefind instance persists across View Transitions ΓÇö no need to re-import
let pagefind = null;

async function loadPagefind() {
Expand Down Expand Up @@ -96,7 +96,7 @@ const base = import.meta.env.BASE_URL;
if (searchResults) searchResults.innerHTML = '';
if (searchEmpty) {
searchResults?.appendChild(searchEmpty);
searchEmpty.textContent = 'Start typing to search…';
searchEmpty.textContent = 'Start typing to search…';
}
searchStatus?.classList.add('hidden');
document.body.style.overflow = '';
Expand All @@ -107,7 +107,7 @@ const base = import.meta.env.BASE_URL;
if (!query) {
if (searchResults && searchEmpty) {
searchResults.innerHTML = '';
searchEmpty.textContent = 'Start typing to search…';
searchEmpty.textContent = 'Start typing to search…';
searchResults.appendChild(searchEmpty);
}
searchStatus?.classList.add('hidden');
Expand Down Expand Up @@ -148,18 +148,18 @@ const base = import.meta.env.BASE_URL;
</div>
<div class="text-sm text-surface-500 dark:text-surface-400 mt-0.5 line-clamp-2">${result.excerpt || ''}</div>
`;
// Close modal on click — navigation proceeds via View Transitions
// Close modal on click ΓÇö navigation proceeds via View Transitions
a.addEventListener('click', closeSearch);
searchResults.appendChild(a);
});
} catch {
if (searchResults) {
searchResults.innerHTML = '<div class="p-8 text-center text-surface-400 text-sm">Search index loading…</div>';
searchResults.innerHTML = '<div class="p-8 text-center text-surface-400 text-sm">Search index loading…</div>';

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback HTML for the loading state contains mojibake ("Search index loading…"). This will render incorrectly; please restore the intended ellipsis character.

Suggested change
searchResults.innerHTML = '<div class="p-8 text-center text-surface-400 text-sm">Search index loading…</div>';
searchResults.innerHTML = '<div class="p-8 text-center text-surface-400 text-sm">Search index loading…</div>';

Copilot uses AI. Check for mistakes.
}
}
}

// Global keydown — register exactly once, never duplicated
// Global keydown ΓÇö register exactly once, never duplicated
let _globalKeydownRegistered = false;
function handleGlobalKeydown(e) {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
Expand Down Expand Up @@ -197,7 +197,7 @@ const base = import.meta.env.BASE_URL;
const isMac = /Mac|iPhone|iPad|iPod/.test(navigator.platform);
const modifierEl = document.getElementById('search-modifier');
if (modifierEl) {
modifierEl.textContent = isMac ? '⌘' : 'Ctrl+';
modifierEl.textContent = isMac ? 'Γîÿ' : 'Ctrl+';
}

// Create fresh bound handlers
Expand Down
77 changes: 37 additions & 40 deletions docs/tests/search.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ test.describe('Search results', () => {
await openSearchViaButton(page);
const input = page.locator('#search-input');
// Use a single random character that pagefind won't match
await input.fill('ÿ');
await input.fill('├┐');

// Wait for search debounce, then check for either "No results" or
// the default "Start typing" (pagefind may not index this character).
Expand All @@ -157,77 +157,74 @@ test.describe('Search results', () => {
await page.waitForTimeout(1500);
const resultsContainer = page.locator('#search-results');
const text = await resultsContainer.textContent();
// Either we get "No results" or actual results — either way the
// Either we get "No results" or actual results ΓÇö either way the
// search pipeline processed the query (not still on "Start typing")
expect(text.trim()).not.toBe('Start typing to search…');
expect(text.trim()).not.toBe('Start typing to search…');
});
});

test.describe('Search after View Transition navigation', () => {

test('search modal works after navigating via search result', async ({ page }) => {
// Initial page load
// Helper: search, click first result, wait for navigation to complete
async function searchAndNavigate(page) {
await page.goto(BASE);

// Open search and perform a query
const searchBtn = page.locator('#search-btn');
await expect(searchBtn).toBeVisible();
await searchBtn.click();

const modal = page.locator('#search-modal');
await expect(modal).not.toHaveClass(/hidden/);

// Type a query and wait for results
const input = page.locator('#search-input');
await input.fill('agent');

const resultLinks = page.locator('#search-results a');
await expect(resultLinks.first()).toBeVisible({ timeout: 10_000 });

// Click first result to navigate (View Transition occurs)
const href = await resultLinks.first().getAttribute('href');
expect(href).toBeTruthy();
await resultLinks.first().click();
await page.waitForURL(new RegExp(href.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), { timeout: 10_000 });

// After navigation, search should still work
const searchBtnAfter = page.locator('#search-btn');
await expect(searchBtnAfter).toBeVisible();
await searchBtnAfter.click();

const modalAfter = page.locator('#search-modal');
await expect(modalAfter).not.toHaveClass(/hidden/);

// Verify input is focused and we can type a new query
const inputAfter = page.locator('#search-input');
await expect(inputAfter).toBeFocused();
await inputAfter.fill('test');

// Results should appear for the new query
const resultLinksAfter = page.locator('#search-results a');
await expect(resultLinksAfter.first()).toBeVisible({ timeout: 10_000 });
});
// Wait for View Transition navigation to complete
await page.waitForURL(new RegExp(href.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), { timeout: 10_000 });
// Allow astro:page-load handlers to re-initialize
await page.waitForTimeout(500);
return href;
}

test('Ctrl+K opens search after View Transition navigation', async ({ page }) => {
// Initial page load
await page.goto(BASE);
test('search modal works after navigating via search result (View Transition)', async ({ page }) => {
await searchAndNavigate(page);

// Perform a search and navigate via result
// On the new page, try to open search again
const searchBtn = page.locator('#search-btn');
await expect(searchBtn).toBeVisible({ timeout: 5_000 });
await searchBtn.click();

const modal = page.locator('#search-modal');
await expect(modal).not.toHaveClass(/hidden/);

// Verify input is focused and can receive text
const input = page.locator('#search-input');
await input.fill('agent');
await expect(input).toBeFocused({ timeout: 3_000 });

// Type a new query and verify results appear
await input.fill('charter');
const resultLinks = page.locator('#search-results a');
await expect(resultLinks.first()).toBeVisible({ timeout: 10_000 });
const count = await resultLinks.count();
expect(count).toBeGreaterThan(0);
});

const href = await resultLinks.first().getAttribute('href');
await resultLinks.first().click();
await page.waitForURL(new RegExp(href.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), { timeout: 10_000 });

// After View Transition navigation, Ctrl+K should open search
const modal = page.locator('#search-modal');
await expect(modal).toHaveClass(/hidden/);
test('Ctrl+K opens search after View Transition navigation', async ({ page }) => {
await searchAndNavigate(page);

// Press Ctrl+K on the post-navigation page
await page.keyboard.press('Control+k');

const modal = page.locator('#search-modal');
await expect(modal).not.toHaveClass(/hidden/);
await expect(page.locator('#search-input')).toBeFocused();

const input = page.locator('#search-input');
await expect(input).toBeFocused({ timeout: 3_000 });
});
});