diff --git a/__tests__/unit/client/theme-default/support/docsearch.test.ts b/__tests__/unit/client/theme-default/support/docsearch.test.ts index 9695760b0..4da8f113f 100644 --- a/__tests__/unit/client/theme-default/support/docsearch.test.ts +++ b/__tests__/unit/client/theme-default/support/docsearch.test.ts @@ -3,7 +3,6 @@ import { hasAskAi, hasKeywordSearch, mergeLangFacetFilters, - resolveDocSearchMode, validateCredentials } from 'client/theme-default/support/docsearch' @@ -41,66 +40,6 @@ describe('client/theme-default/support/docsearch', () => { }) }) - describe('resolveDocSearchMode', () => { - test('defaults to keyword when only keyword search is configured', () => { - expect( - resolveDocSearchMode({ - appId: 'app', - apiKey: 'key', - indexName: 'index', - askAi: undefined, - mode: undefined - }) - ).toBe('keyword') - }) - - test('infers hybrid when keyword search + askAi are configured', () => { - expect( - resolveDocSearchMode({ - appId: 'app', - apiKey: 'key', - indexName: 'index', - askAi: { assistantId: 'assistant' } as any, - mode: 'auto' - }) - ).toBe('hybrid') - }) - - test('infers sidePanel when only askAi is configured', () => { - expect( - resolveDocSearchMode({ - appId: undefined, - apiKey: undefined, - indexName: undefined, - askAi: { assistantId: 'assistant' } as any, - mode: undefined - }) - ).toBe('sidePanel') - }) - - test('respects explicit mode overrides', () => { - expect( - resolveDocSearchMode({ - appId: 'app', - apiKey: 'key', - indexName: 'index', - askAi: { assistantId: 'assistant' } as any, - mode: 'sidePanel' - }) - ).toBe('sidePanel') - - expect( - resolveDocSearchMode({ - appId: undefined, - apiKey: undefined, - indexName: undefined, - askAi: { assistantId: 'assistant' } as any, - mode: 'hybrid' - }) - ).toBe('hybrid') - }) - }) - describe('hasKeywordSearch', () => { test('returns true when all credentials are provided', () => { expect( diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 77a3bceb9..615fea4a8 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -121,7 +121,9 @@ export default defineConfig({ askAi: { assistantId: 'YaVSonfX5bS8', sidePanel: { - pushSelector: '.Layout' + button: { + variant: 'inline' + } } } } diff --git a/package.json b/package.json index f83f1a830..ed07b2222 100644 --- a/package.json +++ b/package.json @@ -95,9 +95,9 @@ "*": "prettier --experimental-cli --ignore-unknown --write" }, "dependencies": { - "@docsearch/css": "^4.5.0-beta.0", - "@docsearch/js": "^4.5.0-beta.0", - "@docsearch/sidepanel-js": "^4.5.0-beta.0", + "@docsearch/css": "^4.5.0-beta.2", + "@docsearch/js": "^4.5.0-beta.2", + "@docsearch/sidepanel-js": "^4.5.0-beta.2", "@iconify-json/simple-icons": "^1.2.59", "@shikijs/core": "^3.15.0", "@shikijs/transformers": "^3.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2b995485..e5a10e026 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,14 +27,14 @@ importers: .: dependencies: '@docsearch/css': - specifier: ^4.5.0-beta.0 - version: 4.5.0-beta.0 + specifier: ^4.5.0-beta.2 + version: 4.5.0-beta.2 '@docsearch/js': - specifier: ^4.5.0-beta.0 - version: 4.5.0-beta.0 + specifier: ^4.5.0-beta.2 + version: 4.5.0-beta.2 '@docsearch/sidepanel-js': - specifier: ^4.5.0-beta.0 - version: 4.5.0-beta.0 + specifier: ^4.5.0-beta.2 + version: 4.5.0-beta.2 '@iconify-json/simple-icons': specifier: ^1.2.59 version: 1.2.59 @@ -399,14 +399,14 @@ packages: conventional-commits-parser: optional: true - '@docsearch/css@4.5.0-beta.0': - resolution: {integrity: sha512-qVJpvv5Qtfiire7TNFdzi+SQ27EjL+qXu5/EMUFSogJeP7K3PruH1OefTjEPygTp6r020hhfG2rLDXcsrVveNw==} + '@docsearch/css@4.5.0-beta.2': + resolution: {integrity: sha512-ChNiH493EnFiYEy8ntrjcGHG9eYDSNa8VJy3dvSom3A9YPmXBC40aoX7yHUfjFxO3Ue5ZNXhTxuhC5uyWgSUww==} - '@docsearch/js@4.5.0-beta.0': - resolution: {integrity: sha512-Z7PZIm00WgOpdhwsRAZ8w6pUJdF/YGGAtKzORS+vz7Z/v63anc/75TH3uxDfnEow8kbqJY8heN/ij86BQQ7yzw==} + '@docsearch/js@4.5.0-beta.2': + resolution: {integrity: sha512-hAt/P7K5eiYW65Xo/4xbDXjLtGgqXEg2y4knz6CUfCcc9m5KnNoi0o1s4lOyP6kehatv3SAiEHoCNGEa+sIqdA==} - '@docsearch/sidepanel-js@4.5.0-beta.0': - resolution: {integrity: sha512-Bh9ZX6+BJvneO1q7lXwpK05vLGeZC4qNxsRgQHwfKCWY0YDKmk0T2qSPt5gVs4i7TMC8G9DbSFMfaFrYqLJR7Q==} + '@docsearch/sidepanel-js@4.5.0-beta.2': + resolution: {integrity: sha512-HGh0tGlnq5RFowvunjLrQmOgBm5vlFPGfoCbZ6d1xQazepc6yCquBcZn5BTyK+f5D6vzjl0dK4plHX1hmMKVLA==} '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} @@ -3254,11 +3254,11 @@ snapshots: conventional-commits-filter: 5.0.0 conventional-commits-parser: 6.2.1 - '@docsearch/css@4.5.0-beta.0': {} + '@docsearch/css@4.5.0-beta.2': {} - '@docsearch/js@4.5.0-beta.0': {} + '@docsearch/js@4.5.0-beta.2': {} - '@docsearch/sidepanel-js@4.5.0-beta.0': {} + '@docsearch/sidepanel-js@4.5.0-beta.2': {} '@emnapi/core@1.7.1': dependencies: diff --git a/src/client/theme-default/components/VPAlgoliaSearchBox.vue b/src/client/theme-default/components/VPAlgoliaSearchBox.vue index d82d24042..72d1c0a18 100644 --- a/src/client/theme-default/components/VPAlgoliaSearchBox.vue +++ b/src/client/theme-default/components/VPAlgoliaSearchBox.vue @@ -1,26 +1,28 @@ - @@ -58,5 +48,17 @@ .DocSearch-SidepanelButton { display: none !important; } - +.VPNavBarAskAiButton { + transition: all 0.25s; +} + + +.VPNavBarAskAiButton:hover { + background: var(--docsearch-primary-color); +} + +.VPNavBarAskAiButton:hover .DocSearch-Search-Icon { + background-color: var(--vp-c-white); +} + diff --git a/src/client/theme-default/components/VPNavBarSearch.vue b/src/client/theme-default/components/VPNavBarSearch.vue index 421339553..2f6751c8d 100644 --- a/src/client/theme-default/components/VPNavBarSearch.vue +++ b/src/client/theme-default/components/VPNavBarSearch.vue @@ -5,7 +5,7 @@ import { onKeyStroke } from '@vueuse/core' import type { DefaultTheme } from 'vitepress/theme' import { computed, defineAsyncComponent, onMounted, onUnmounted, ref } from 'vue' import { useData } from '../composables/data' -import { hasKeywordSearch, resolveDocSearchMode } from '../support/docsearch' +import { hasKeywordSearch } from '../support/docsearch' import VPNavBarAskAiButton from './VPNavBarAskAiButton.vue' import VPNavBarSearchButton from './VPNavBarSearchButton.vue' @@ -30,10 +30,8 @@ const algoliaOptions = computed(() => { } as DefaultTheme.AlgoliaSearchOptions }) -const resolvedAlgoliaMode = computed(() => resolveDocSearchMode(algoliaOptions.value)) const showKeywordSearchButton = computed( () => - resolvedAlgoliaMode.value !== 'sidePanel' && hasKeywordSearch(algoliaOptions.value) ) @@ -49,7 +47,10 @@ const askAiShortcutEnabled = computed(() => { return cfg?.keyboardShortcuts?.['Ctrl/Cmd+I'] !== false }) -let isProgrammaticOpen = false +type OpenTarget = 'search' | 'askAi' | 'toggleAskAi' +type OpenRequest = { target: OpenTarget; nonce: number } +const openRequest = ref(null) +let openNonce = 0 // to avoid loading the docsearch js upfront (which is more than 1/3 of the // payload), we delay initializing it until the user has actually clicked or @@ -89,8 +90,6 @@ onMounted(() => { preconnect() const handleSearchHotKey = (event: KeyboardEvent) => { - if (isProgrammaticOpen) return - const key = event.key?.toLowerCase() if ( @@ -99,7 +98,6 @@ onMounted(() => { (!isEditingContent(event) && event.key === '/')) ) { event.preventDefault() - remove() loadAndOpen('search') return } @@ -112,7 +110,6 @@ onMounted(() => { !isEditingContent(event) ) { event.preventDefault() - remove() loadAndOpen('askAi') } } @@ -126,61 +123,14 @@ onMounted(() => { onUnmounted(remove) }) -type OpenTarget = 'search' | 'askAi' - -function programmaticOpen(target: OpenTarget) { - isProgrammaticOpen = true - open(target) - queueMicrotask(() => { - isProgrammaticOpen = false - }) -} - function loadAndOpen(target: OpenTarget) { if (!loaded.value) { loaded.value = true - setTimeout(() => pollOpen(target, 0), 16) - return } - programmaticOpen(target) -} - -function open(target: OpenTarget) { - const e = new Event('keydown') as any - e.key = target === 'search' ? 'k' : 'i' - e.metaKey = true - e.ctrlKey = true - - window.dispatchEvent(e) -} - -function pollOpen(target: OpenTarget, tries: number) { - // For askAi, first wait until sidepanel-js has rendered its button - if (target === 'askAi') { - const sidepanelReady = document.querySelector( - '#docsearch-sidepanel .DocSearchSidepanel-Button, #docsearch-sidepanel [class*="Sidepanel"]' - ) - if (!sidepanelReady) { - if (tries < 120) { - setTimeout(() => pollOpen(target, tries + 1), 16) - } - return - } - } - - programmaticOpen(target) - - setTimeout(() => { - const opened = - target === 'search' - ? Boolean(document.querySelector('.DocSearch-Modal')) - : Boolean(document.querySelector('[class*="Sidepanel"][class*="open"], [class*="Sidepanel"][class*="visible"]')) - - if (opened) return - if (tries >= 120) return - pollOpen(target, tries + 1) - }, 16) + // This will either be handled immediately if DocSearch is ready, + // or queued by the AlgoliaSearchBox until its instances become ready. + openRequest.value = { target, nonce: ++openNonce } } function isEditingContent(event: KeyboardEvent): boolean { @@ -221,10 +171,7 @@ const provider = __ALGOLIA__ ? 'algolia' : __VP_LOCAL_SEARCH__ ? 'local' : '' @@ -40,7 +37,7 @@ const translate = createSearchTranslate(defaultTranslations) --docsearch-focus-color: var(--vp-c-brand-1); --docsearch-footer-background: var(--vp-c-bg); --docsearch-highlight-color: var(--vp-c-brand-1); - --docsearch-hit-background: var(--vp-c-default-soft); + --docsearch-hit-background: var(--vp-c-bg); --docsearch-hit-color: var(--vp-c-text-1); --docsearch-hit-highlight-color: var(--vp-c-brand-soft); --docsearch-icon-color: var(--vp-c-text-2); @@ -92,7 +89,7 @@ const translate = createSearchTranslate(defaultTranslations) --docsearch-muted-color: var(--docsearch-text-color); --docsearch-searchbox-background: transparent; width: auto; - padding: 2px 12px; + padding: 0px 8px; border: none; border-radius: 8px; } diff --git a/src/client/theme-default/styles/icons.css b/src/client/theme-default/styles/icons.css index 62c737931..cb056fafd 100644 --- a/src/client/theme-default/styles/icons.css +++ b/src/client/theme-default/styles/icons.css @@ -76,9 +76,11 @@ .vpi-search { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.6'%3E%3Cpath d='m21 21l-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8' stroke-width='1.4'/%3E%3C/g%3E%3C/svg%3E"); } + .vpi-sparkles { - --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.4'%3E%3Cpath d='M12 2l1.7 6.3L20 10l-6.3 1.7L12 18l-1.7-6.3L4 10l6.3-1.7z'/%3E%3Cpath d='M5 15l.8 3.2L9 19l-3.2.8L5 23l-.8-3.2L1 19l3.2-.8z'/%3E%3C/g%3E%3C/svg%3E"); + --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.3'%3E%3Cpath d='M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z'/%3E%3Cpath d='M20 3v4'/%3E%3Cpath d='M22 5h-4'/%3E%3Cpath d='M4 17v2'/%3E%3Cpath d='M5 18H3'/%3E%3C/g%3E%3C/svg%3E"); } + .vpi-layout-list { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Crect width='7' height='7' x='3' y='3' rx='1'/%3E%3Crect width='7' height='7' x='3' y='14' rx='1'/%3E%3Cpath d='M14 4h7m-7 5h7m-7 6h7m-7 5h7'/%3E%3C/g%3E%3C/svg%3E"); } diff --git a/src/client/theme-default/support/docsearch.ts b/src/client/theme-default/support/docsearch.ts index 7404b601b..eb5f265d5 100644 --- a/src/client/theme-default/support/docsearch.ts +++ b/src/client/theme-default/support/docsearch.ts @@ -1,6 +1,5 @@ import type { DefaultTheme } from 'vitepress/theme' -export type ResolvedDocSearchMode = 'keyword' | 'hybrid' | 'sidePanel' export type FacetFilter = string | string[] export interface ValidatedCredentials { @@ -27,22 +26,6 @@ export function hasAskAi( return Boolean(askAi.assistantId) } -export function resolveDocSearchMode( - options: Pick< - DefaultTheme.AlgoliaSearchOptions, - 'mode' | 'appId' | 'apiKey' | 'indexName' | 'askAi' - > -): ResolvedDocSearchMode { - if (options.mode === 'sidePanel') return 'sidePanel' - if (options.mode === 'hybrid') return 'hybrid' - - // auto (default) - const keyword = hasKeywordSearch(options) - const askAiEnabled = hasAskAi(options.askAi) - if (askAiEnabled) return keyword ? 'hybrid' : 'sidePanel' - return 'keyword' -} - /** * Removes existing `lang:` filters and appends `lang:${lang}`. * Handles both flat arrays and nested arrays (for OR conditions). diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 7faeb1828..957f0abb9 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -147,7 +147,7 @@ export async function createVitePressPlugin( 'vitepress > @vue/devtools-api', 'vitepress > @vueuse/core' ].filter((d) => d != null), - exclude: ['@docsearch/js', 'vitepress'] + exclude: ['@docsearch/js', '@docsearch/sidepanel-js', 'vitepress'] }, server: { fs: {