) {
- const { query, item, active, ...rest } = props;
+ const { query, item, active, style, ...rest } = props;
const language = useLanguage();
const domain = getDomain(item.href);
@@ -41,6 +42,7 @@ export const SearchRecordResultItem = React.forwardRef(function SearchRecordResu
},
}}
aria-label={tString(language, 'search_page_result_title', item.title)}
+ style={{ ...style }}
{...rest}
>
diff --git a/packages/gitbook/src/components/Search/SearchResultItem.tsx b/packages/gitbook/src/components/Search/SearchResultItem.tsx
index 5ef500d7a..02343f80f 100644
--- a/packages/gitbook/src/components/Search/SearchResultItem.tsx
+++ b/packages/gitbook/src/components/Search/SearchResultItem.tsx
@@ -13,6 +13,7 @@ export const SearchResultItem = React.forwardRef(function SearchResultItem(
className?: string;
size?: 'small' | 'medium';
leadingIcon?: React.ReactNode;
+ disabled?: boolean;
} & LinkInsightsProps &
Omit, keyof LinkInsightsProps>,
ref: React.Ref
@@ -25,23 +26,25 @@ export const SearchResultItem = React.forwardRef(function SearchResultItem(
leadingIcon,
size = 'medium',
action,
+ disabled,
...rest
} = props;
return (
{children}
- {active ? (
+ {disabled ? null : active ? (
) : (
-
+
)}
diff --git a/packages/gitbook/src/components/Search/SearchResults.tsx b/packages/gitbook/src/components/Search/SearchResults.tsx
index 1c8a03833..9a0d6886e 100644
--- a/packages/gitbook/src/components/Search/SearchResults.tsx
+++ b/packages/gitbook/src/components/Search/SearchResults.tsx
@@ -1,21 +1,23 @@
'use client';
import assertNever from 'assert-never';
+import { AnimatePresence, motion } from 'framer-motion';
import React from 'react';
import { useAI } from '@/components/AI';
import { t, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
-import { Button, Loading } from '../primitives';
+import { Button, SkeletonParagraph, SkeletonSmall } from '../primitives';
import { SearchPageResultItem } from './SearchPageResultItem';
import { SearchQuestionResultItem } from './SearchQuestionResultItem';
import { SearchRecordResultItem } from './SearchRecordResultItem';
+import { SearchResultItem } from './SearchResultItem';
import type { OrderedComputedResult } from './search-types';
import type { LocalPageResult } from './useLocalSearchResults';
export interface SearchResultsRef {
- select(): void;
+ select(): boolean;
}
type ResultType =
@@ -23,6 +25,21 @@ type ResultType =
| LocalPageResult
| { type: 'recommended-question'; id: string; question: string };
+function getResultKey(item: ResultType): string {
+ switch (item.type) {
+ case 'local-page':
+ return `page:${item.id}`;
+ case 'page':
+ return `page:${item.pageId}`;
+ case 'record':
+ return `record:${item.id}`;
+ case 'recommended-question':
+ return `question:${item.id}`;
+ default:
+ return assertNever(item);
+ }
+}
+
/**
* Fetch the results of the keyboard navigable elements to display for a query:
* - Recommended questions if no query is provided.
@@ -37,15 +54,31 @@ export const SearchResults = React.forwardRef(function SearchResults(
fetching: boolean;
cursor: number | null;
error: boolean;
+ onResultSelect?: () => void;
},
ref: React.Ref
) {
- const { children, id, query, results, fetching, cursor, error } = props;
+ const { children, id, query, results, fetching, cursor, error, onResultSelect } = props;
const language = useLanguage();
+ const shouldAnimateResults = !query || fetching;
+ const previousCursor = React.useRef(cursor);
+ const seenResultKeys = React.useRef(new Set());
+ const lastQuery = React.useRef(query);
+
+ if (lastQuery.current !== query) {
+ lastQuery.current = query;
+ seenResultKeys.current.clear();
+ }
const refs = React.useRef<(null | HTMLAnchorElement)[]>([]);
+ React.useEffect(() => {
+ for (const item of results) {
+ seenResultKeys.current.add(getResultKey(item));
+ }
+ }, [results]);
+
// Scroll to the active result.
React.useEffect(() => {
if (cursor === null || !refs.current[cursor]) {
@@ -55,15 +88,22 @@ export const SearchResults = React.forwardRef(function SearchResults(
refs.current[cursor]?.scrollIntoView({
block: 'nearest',
inline: 'nearest',
+ behavior: 'instant',
});
}, [cursor]);
+ const shouldDisableLayoutAnimation = previousCursor.current !== cursor;
+ React.useEffect(() => {
+ previousCursor.current = cursor;
+ }, [cursor]);
+
const select = React.useCallback(() => {
if (cursor === null || !refs.current[cursor]) {
- return;
+ return false;
}
refs.current[cursor]?.click();
+ return true;
}, [cursor]);
React.useImperativeHandle(
@@ -75,18 +115,19 @@ export const SearchResults = React.forwardRef(function SearchResults(
);
const { assistants } = useAI();
+ const primaryAssistant = assistants[0];
if (error) {
return (
-
@@ -99,27 +140,28 @@ export const SearchResults = React.forwardRef(function SearchResults(
>
{t(language, 'unexpected_error_retry')}
-
+
);
}
const noResults = (
-
{t(language, 'search_no_results_for', query)}
-
+
);
return (
-
+
);
});
+
+const SearchResultsSkeleton = (props: { items: number }) => {
+ const { items } = props;
+ const skeletonKeys = Array.from({ length: items }, (_, index) => `skeleton:${index}`);
+
+ return (
+ <>
+ {skeletonKeys.map((key, index) => (
+
+ }
+ >
+
+
+ ))}
+ >
+ );
+};
diff --git a/packages/gitbook/src/components/Search/SearchScopeControl.tsx b/packages/gitbook/src/components/Search/SearchScopeControl.tsx
index 47de55a8f..5b3fb27cf 100644
--- a/packages/gitbook/src/components/Search/SearchScopeControl.tsx
+++ b/packages/gitbook/src/components/Search/SearchScopeControl.tsx
@@ -22,7 +22,7 @@ export function SearchScopeControl(props: SearchScopeControlProps) {
const state = useSearchState();
- if (!state) {
+ if (!state?.open) {
return null;
}
@@ -31,7 +31,7 @@ export function SearchScopeControl(props: SearchScopeControlProps) {
const variantScopeIsExtended = ['extended', 'all'].includes(state.scope);
return (
-
+
{withSections ? (
diff --git a/packages/gitbook/src/components/Search/useSearchController.tsx b/packages/gitbook/src/components/Search/useSearchController.tsx
index 05a6c7465..3768c4202 100644
--- a/packages/gitbook/src/components/Search/useSearchController.tsx
+++ b/packages/gitbook/src/components/Search/useSearchController.tsx
@@ -62,11 +62,15 @@ function useSearchKeyboardNavigation(props: {
query: string;
results: ReturnType
['results'];
resultsRef: React.RefObject;
+ abort: () => void;
+ askCount: number;
+ onAskSelect: (index: number) => void;
}) {
- const { query, results, resultsRef } = props;
+ const { query, results, resultsRef, abort, askCount, onAskSelect } = props;
const { cursor, moveBy: moveCursorBy } = useSearchResultsCursor({
query,
- results,
+ resultCount: results.length,
+ totalCount: results.length + askCount,
});
const onInputKeyDown = React.useCallback(
@@ -79,10 +83,18 @@ function useSearchKeyboardNavigation(props: {
moveCursorBy(1);
} else if (event.key === 'Enter') {
event.preventDefault();
- resultsRef.current?.select();
+ if (cursor !== null && cursor >= results.length) {
+ onAskSelect(cursor - results.length);
+ return;
+ }
+
+ if (resultsRef.current?.select()) {
+ // Stop any in-flight search request only when activating a result.
+ abort();
+ }
}
},
- [moveCursorBy, resultsRef]
+ [moveCursorBy, cursor, results.length, onAskSelect, resultsRef, abort]
);
return {
@@ -179,7 +191,7 @@ export function useSearchController(props: SearchBaseProps) {
language: siteSpace.space.language,
});
- const { results, fetching, error } = useSearchResults({
+ const { results, fetching, error, abort } = useSearchResults({
asEmbeddable,
disabled: !(state?.query || withAI),
query: normalizedQuery,
@@ -195,18 +207,45 @@ export function useSearchController(props: SearchBaseProps) {
const searchValue = state?.query ?? (withSearchAI || !withAI ? state?.ask : null) ?? '';
const searchResultsId = `search-results-${React.useId()}`;
+ const askInAssistant = React.useCallback(
+ (assistantIndex = 0) => {
+ const assistant = assistants[assistantIndex];
+ if (!assistant || !normalizedQuery) {
+ return;
+ }
+
+ abort();
+ assistant.open(normalizedQuery);
+ setSearchState({
+ ask: normalizedQuery,
+ query: null,
+ scope: state?.scope ?? 'default',
+ open: assistant.mode === 'search',
+ });
+ },
+ [abort, assistants, normalizedQuery, setSearchState, state?.scope]
+ );
+
+ const askCount = normalizedQuery && !showAsk ? assistants.length : 0;
+
const { cursor, onInputKeyDown } = useSearchKeyboardNavigation({
query: normalizedQuery,
results,
resultsRef,
+ abort,
+ askCount,
+ onAskSelect: askInAssistant,
});
return {
assistants,
askQuery: normalizedAsk,
+ askCount,
+ askInAssistant,
cursor,
error,
fetching,
+ abort,
open: onOpen,
close: onClose,
query: normalizedQuery,
diff --git a/packages/gitbook/src/components/Search/useSearchResults.ts b/packages/gitbook/src/components/Search/useSearchResults.ts
index 6a81419f3..810f807e2 100644
--- a/packages/gitbook/src/components/Search/useSearchResults.ts
+++ b/packages/gitbook/src/components/Search/useSearchResults.ts
@@ -84,6 +84,10 @@ export function useSearchResults(props: {
error: boolean;
}>({ results: [], fetching: false, error: false });
+ // Track the current in-flight fetch so it can be aborted imperatively
+ // when the user navigates away before the request completes.
+ const abortRef = React.useRef<(() => void) | null>(null);
+
const { assistants } = useAI();
const withAI = assistants.length > 0;
@@ -227,10 +231,17 @@ export function useSearchResults(props: {
}
}, 200);
+ abortRef.current = () => {
+ cancelled = true;
+ clearTimeout(timeout);
+ abortController.abort();
+ };
+
return () => {
cancelled = true;
clearTimeout(timeout);
abortController.abort();
+ abortRef.current = null;
};
}, [
query,
@@ -245,6 +256,12 @@ export function useSearchResults(props: {
asEmbeddable,
]);
+ const abort = React.useCallback(() => {
+ abortRef.current?.();
+ abortRef.current = null;
+ setRemoteState((prev) => (prev.fetching ? { ...prev, fetching: false } : prev));
+ }, []);
+
// Merge local and remote results.
// Re-runs immediately whenever either result set changes.
const results = React.useMemo(() => {
@@ -272,6 +289,7 @@ export function useSearchResults(props: {
results,
fetching: remoteState.fetching,
error: remoteState.error,
+ abort,
};
}
diff --git a/packages/gitbook/src/components/Search/useSearchResultsCursor.ts b/packages/gitbook/src/components/Search/useSearchResultsCursor.ts
index db369c0f6..9cc27d093 100644
--- a/packages/gitbook/src/components/Search/useSearchResultsCursor.ts
+++ b/packages/gitbook/src/components/Search/useSearchResultsCursor.ts
@@ -1,9 +1,12 @@
import React from 'react';
-import type { ResultType } from './useSearchResults';
-export function useSearchResultsCursor(props: { query: string; results: ResultType[] }) {
+export function useSearchResultsCursor(props: {
+ query: string;
+ resultCount: number;
+ totalCount: number;
+}) {
const [cursor, setCursor] = React.useState(null);
- const { query, results } = props;
+ const { query, resultCount, totalCount } = props;
React.useEffect(() => {
if (!query) {
@@ -13,22 +16,48 @@ export function useSearchResultsCursor(props: { query: string; results: ResultTy
}, [query]);
React.useEffect(() => {
- if (results.length > 0) {
- // Auto-focus the first result
- setCursor(0);
+ if (totalCount === 0) {
+ return;
}
- }, [results]);
+
+ setCursor((prev) => {
+ if (resultCount > 0) {
+ return 0;
+ }
+
+ return prev ?? 0;
+ });
+ }, [resultCount, totalCount]);
+
+ React.useEffect(() => {
+ setCursor((prev) => {
+ if (prev === null) {
+ return prev;
+ }
+
+ if (totalCount === 0) {
+ return null;
+ }
+
+ return Math.min(prev, totalCount - 1);
+ });
+ }, [totalCount]);
const moveBy = React.useCallback(
(delta: number) => {
setCursor((prev) => {
+ if (totalCount === 0) {
+ return null;
+ }
+
if (prev === null) {
return 0;
}
- return Math.max(Math.min(prev + delta, results.length - 1), 0);
+
+ return Math.max(Math.min(prev + delta, totalCount - 1), 0);
});
},
- [results]
+ [totalCount]
);
return {
diff --git a/packages/gitbook/src/components/SitePage/SitePageSkeleton.tsx b/packages/gitbook/src/components/SitePage/SitePageSkeleton.tsx
index 42e4311ea..123a4a2e8 100644
--- a/packages/gitbook/src/components/SitePage/SitePageSkeleton.tsx
+++ b/packages/gitbook/src/components/SitePage/SitePageSkeleton.tsx
@@ -22,7 +22,7 @@ export function SitePageSkeleton() {
>
-
+
);
diff --git a/packages/gitbook/src/components/primitives/KeyboardShortcut.tsx b/packages/gitbook/src/components/primitives/KeyboardShortcut.tsx
index 4042f0b89..35c2520f8 100644
--- a/packages/gitbook/src/components/primitives/KeyboardShortcut.tsx
+++ b/packages/gitbook/src/components/primitives/KeyboardShortcut.tsx
@@ -14,7 +14,7 @@ export function KeyboardShortcut(props: KeyboardShortcutProps) {
return (
{keys.map((key, index) => {
@@ -33,6 +33,14 @@ export function KeyboardShortcut(props: KeyboardShortcutProps) {
case 'enter':
element =
;
break;
+
+ case 'up':
+ element =
;
+ break;
+
+ case 'down':
+ element =
;
+ break;
}
return (
(null);
+ const [portalTarget, setPortalTarget] = React.useState(null);
// Internal state for uncontrolled mode (only used when open prop is undefined)
const [open, setOpen] = React.useState(openState ?? false);
@@ -72,6 +74,15 @@ export function SideSheet(
const [shouldHide, setShouldHide] = React.useState(!isOpen);
// Track if component has been opened to prevent initial animation
+ React.useEffect(() => {
+ if (isModal) {
+ setPortalTarget(document.body);
+ return;
+ }
+
+ setPortalTarget(null);
+ }, [isModal]);
+
React.useEffect(() => {
let timer: ReturnType | undefined;
if (isOpen) {
@@ -191,7 +202,7 @@ export function SideSheet(
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isModal, isOpen]);
- return (
+ const content = (
<>
{withOverlay ? (
>
);
+
+ if (isModal && portalTarget) {
+ return createPortal(content, portalTarget);
+ }
+
+ return content;
}
/** Backdrop overlay shown behind the modal sheet */
diff --git a/packages/gitbook/src/components/primitives/Skeleton.tsx b/packages/gitbook/src/components/primitives/Skeleton.tsx
index 1b1369e1a..03c355f78 100644
--- a/packages/gitbook/src/components/primitives/Skeleton.tsx
+++ b/packages/gitbook/src/components/primitives/Skeleton.tsx
@@ -7,13 +7,49 @@ import { LoadingPane } from './LoadingPane';
* Placeholder to be used when a content is not yet loaded (in a React.Suspense boundary).
* It's used when streaming the content of a page.
*/
-export function SkeletonParagraph(props: { id?: string; style?: ClassValue }) {
- const { id, style } = props;
+export function SkeletonParagraph(props: {
+ lines?: number;
+ id?: string;
+ size?: 'xsmall' | 'small' | 'medium';
+ start?: number;
+ className?: ClassValue;
+ style?: React.CSSProperties;
+}) {
+ const { lines = 3, id, size = 'medium', start = 0, className, style } = props;
+
+ const lineHeight = size === 'small' ? 'h-5' : 'h-6';
+ const itemHeight = size === 'small' ? 'h-3' : 'h-4';
+ const wordGap = size === 'small' ? 'gap-1' : 'gap-2';
+
return (
-
-
+
+ {Array.from({ length: lines }).map((_, line) => {
+ const itemsPerLine = (line % 2) + 2;
+ return (
+
+ {Array.from({ length: itemsPerLine }).map((_, item) => (
+
+ ))}
+
+ );
+ })}
);
}
@@ -62,29 +98,45 @@ export function SkeletonCard(props: { id?: string; style?: ClassValue }) {
/**
* Placeholder when loading small elements
*/
-export function SkeletonSmall(props: { id?: string; style?: ClassValue }) {
- const { id, style } = props;
- return (
-
-
-
- );
+export function SkeletonSmall(
+ props: { id?: string; className?: ClassValue } & React.ComponentProps<'div'>
+) {
+ const { id, className, ...rest } = props;
+
+ return
;
}
/**
* Placeholder when loading an Update block
*/
-export function SkeletonUpdate(props: { id?: string; style?: ClassValue }) {
- const { id, style } = props;
+export function SkeletonUpdate(props: { id?: string; className?: ClassValue }) {
+ const { id, className } = props;
return (
-
+
);
}
+
+function LoadingItem(props: React.ComponentProps<'div'>): React.ReactNode {
+ const { className, ...rest } = props;
+
+ return (
+
+ );
+}
diff --git a/packages/gitbook/src/intl/translations/de.ts b/packages/gitbook/src/intl/translations/de.ts
index 66ef06765..3907a2971 100644
--- a/packages/gitbook/src/intl/translations/de.ts
+++ b/packages/gitbook/src/intl/translations/de.ts
@@ -30,6 +30,7 @@ export const de = {
search_scope_variant_all: 'Alle Inhalte',
search_scope_variant_all_description: 'Jede verfügbare Variante',
ask: 'Fragen',
+ ai_chat_ask_query: 'An ${1} fragen: "${2}"',
search_ask: 'Fragen "${1}"',
search_ask_description: 'Finden Sie die Antwort mit ${1}',
search_ask_sources: 'Quellen',
diff --git a/packages/gitbook/src/intl/translations/en.ts b/packages/gitbook/src/intl/translations/en.ts
index 8f455e456..34cbc48c2 100644
--- a/packages/gitbook/src/intl/translations/en.ts
+++ b/packages/gitbook/src/intl/translations/en.ts
@@ -134,6 +134,7 @@ export const en = {
ai_chat_tools_mcp_tool: 'Called ${1}',
ai_chat_ask: 'Ask ${1}',
ai_chat_ask_about_page: 'Ask ${1} about this page',
+ ai_chat_ask_query: 'Ask ${1} "${2}"',
copy_for_llms: 'Copy for LLMs',
copy_page_markdown: 'Copy page as Markdown for LLMs',
copy_page: 'Copy page',
diff --git a/packages/gitbook/src/intl/translations/es.ts b/packages/gitbook/src/intl/translations/es.ts
index 289a5f5a1..24c0680b0 100644
--- a/packages/gitbook/src/intl/translations/es.ts
+++ b/packages/gitbook/src/intl/translations/es.ts
@@ -32,6 +32,7 @@ export const es: TranslationLanguage = {
search_scope_variant_all: 'Todo el contenido',
search_scope_variant_all_description: 'Cada variante disponible',
ask: 'Preguntar',
+ ai_chat_ask_query: 'Preguntar a ${1} "${2}"',
search_ask: 'Preguntar "${1}"',
search_ask_description: 'Encuentra la respuesta con ${1}',
search_ask_sources: 'Fuentes',
diff --git a/packages/gitbook/src/intl/translations/fr.ts b/packages/gitbook/src/intl/translations/fr.ts
index 44978aeb4..161611fc6 100644
--- a/packages/gitbook/src/intl/translations/fr.ts
+++ b/packages/gitbook/src/intl/translations/fr.ts
@@ -30,6 +30,7 @@ export const fr = {
search_scope_variant_all: 'Tout le contenu',
search_scope_variant_all_description: 'Chaque variante disponible',
ask: 'Une question ?',
+ ai_chat_ask_query: 'Demander à ${1} « ${2} »',
search_ask: 'Demander « ${1} »',
search_ask_description: 'Trouvez la réponse avec ${1}',
search_ask_sources: 'Sources',
diff --git a/packages/gitbook/src/intl/translations/it.ts b/packages/gitbook/src/intl/translations/it.ts
index 75e086698..cbbe2ad0c 100644
--- a/packages/gitbook/src/intl/translations/it.ts
+++ b/packages/gitbook/src/intl/translations/it.ts
@@ -32,6 +32,7 @@ export const it: TranslationLanguage = {
search_scope_variant_all: 'Tutti i contenuti',
search_scope_variant_all_description: 'Ogni variante disponibile',
ask: 'Chiedi',
+ ai_chat_ask_query: 'Chiedi a ${1} "${2}"',
search_ask: 'Chiedi "${1}"',
search_ask_description: 'Trova la risposta con ${1}',
search_ask_sources: 'Fonti',
diff --git a/packages/gitbook/src/intl/translations/ja.ts b/packages/gitbook/src/intl/translations/ja.ts
index e267df6b8..5acb0d311 100644
--- a/packages/gitbook/src/intl/translations/ja.ts
+++ b/packages/gitbook/src/intl/translations/ja.ts
@@ -32,6 +32,7 @@ export const ja: TranslationLanguage = {
search_scope_variant_all: 'すべてのコンテンツ',
search_scope_variant_all_description: '利用可能なすべてのバリエーション',
ask: '質問する',
+ ai_chat_ask_query: '"${2}" を ${1} に質問する',
search_ask: '"${1}" を質問する',
search_ask_description: '${1}で答えを見つける',
search_ask_sources: '情報源',
diff --git a/packages/gitbook/src/intl/translations/ko.ts b/packages/gitbook/src/intl/translations/ko.ts
index 7d3520277..6fe819c51 100644
--- a/packages/gitbook/src/intl/translations/ko.ts
+++ b/packages/gitbook/src/intl/translations/ko.ts
@@ -32,6 +32,7 @@ export const ko: TranslationLanguage = {
search_scope_variant_all: '전체 콘텐츠',
search_scope_variant_all_description: '사용 가능한 모든 변형',
ask: '질문',
+ ai_chat_ask_query: '${1}에게 "${2}" 질문하기',
search_ask: '"${1}" 질문하기',
search_ask_description: '${1}로 답변 찾기',
search_ask_sources: '출처',
diff --git a/packages/gitbook/src/intl/translations/nl.ts b/packages/gitbook/src/intl/translations/nl.ts
index 8458210a9..6b39688d4 100644
--- a/packages/gitbook/src/intl/translations/nl.ts
+++ b/packages/gitbook/src/intl/translations/nl.ts
@@ -32,6 +32,7 @@ export const nl: TranslationLanguage = {
search_scope_variant_all: 'Alle inhoud',
search_scope_variant_all_description: 'Elke beschikbare variant',
ask: 'Vragen',
+ ai_chat_ask_query: 'Vraag ${1} "${2}"',
search_ask: 'Vraag "${1}"',
search_ask_description: 'Vind het antwoord met ${1}',
search_ask_sources: 'Bronnen',
diff --git a/packages/gitbook/src/intl/translations/no.ts b/packages/gitbook/src/intl/translations/no.ts
index 07e69bf95..72eaf2f29 100644
--- a/packages/gitbook/src/intl/translations/no.ts
+++ b/packages/gitbook/src/intl/translations/no.ts
@@ -32,6 +32,7 @@ export const no: TranslationLanguage = {
search_scope_variant_all: 'Alt innhold',
search_scope_variant_all_description: 'Hver tilgjengelig variant',
ask: 'Spør',
+ ai_chat_ask_query: 'Spør ${1} "${2}"',
search_ask: 'Spør "${1}"',
search_ask_description: 'Finn svaret med ${1}',
search_ask_sources: 'Kilder',
diff --git a/packages/gitbook/src/intl/translations/pt-br.ts b/packages/gitbook/src/intl/translations/pt-br.ts
index b89d30c3a..a328f0029 100644
--- a/packages/gitbook/src/intl/translations/pt-br.ts
+++ b/packages/gitbook/src/intl/translations/pt-br.ts
@@ -30,6 +30,7 @@ export const pt_br = {
search_scope_variant_all: 'Todo o conteúdo',
search_scope_variant_all_description: 'Cada variante disponível',
ask: 'Perguntar',
+ ai_chat_ask_query: 'Perguntar a ${1} "${2}"',
search_ask: 'Perguntar "${1}"',
search_ask_description: 'Encontre a resposta com ${1}',
search_ask_sources: 'Fontes',
diff --git a/packages/gitbook/src/intl/translations/ru.ts b/packages/gitbook/src/intl/translations/ru.ts
index 40be82814..ad5b4ab84 100644
--- a/packages/gitbook/src/intl/translations/ru.ts
+++ b/packages/gitbook/src/intl/translations/ru.ts
@@ -30,6 +30,7 @@ export const ru = {
search_scope_variant_all: 'Весь контент',
search_scope_variant_all_description: 'Каждый доступный вариант',
ask: 'Спросить',
+ ai_chat_ask_query: 'Спросить у ${1} "${2}"',
search_ask: 'Спросить "${1}"',
search_ask_description: 'Найти ответ с помощью ${1}',
search_ask_sources: 'Источники',
diff --git a/packages/gitbook/src/intl/translations/zh.ts b/packages/gitbook/src/intl/translations/zh.ts
index 2f14d6065..db14f74f7 100644
--- a/packages/gitbook/src/intl/translations/zh.ts
+++ b/packages/gitbook/src/intl/translations/zh.ts
@@ -32,6 +32,7 @@ export const zh: TranslationLanguage = {
search_scope_variant_all: '所有内容',
search_scope_variant_all_description: '每个可用的变体',
ask: '询问',
+ ai_chat_ask_query: '向${1}询问"${2}"',
search_ask: '询问"${1}"',
search_ask_description: '利用${1}找到答案',
search_ask_sources: '来源',
diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts
index dd16a870a..270e50a5a 100644
--- a/packages/gitbook/tailwind.config.ts
+++ b/packages/gitbook/tailwind.config.ts
@@ -314,23 +314,29 @@ const config: Config = {
'fadeIn-slow': 'fadeIn 500ms ease both',
fadeOut: 'fadeOut 200ms ease both',
'fadeOut-slow': 'fadeOut 500ms ease both',
- appear: 'appear 200ms ease both allow-discrete',
+ appear: 'fadeIn 1ms ease both',
- blurIn: 'blurIn 200ms ease-out both',
- blurInSlow: 'blurIn 500ms ease-out both',
- blurOut: 'blurOut 200ms ease-in both',
- blurOutSlow: 'blurOut 500ms ease-in both',
- blurInDisplay: 'blurInDisplay 200ms ease-out both',
- blurInDisplaySlow: 'blurInDisplay 500ms ease-out both',
- blurOutDisplay: 'blurOutDisplay 200ms ease-in both',
- blurOutDisplaySlow: 'blurOutDisplay 500ms ease-in both',
+ blurIn: 'blurIn 200ms both',
+ blurInSlow: 'blurIn 500ms both',
+ blurOut: 'blurOut 200ms both',
+ blurOutSlow: 'blurOut 500ms both',
+ blurInDisplay: 'blurInDisplay 200ms both',
+ blurInDisplaySlow: 'blurInDisplay 500ms both',
+ blurOutDisplay: 'blurOutDisplay 200ms both',
+ blurOutDisplaySlow: 'blurOutDisplay 500ms both',
+
+ blurInHeight: 'heightIn 200ms ease-out both, blurIn 200ms 100ms both',
+ blurInHeightSlow: 'heightIn 500ms ease-out both, blurIn 500ms 300ms both',
+ blurOutHeight: 'blurOut 200ms both, heightOut 200ms 100ms ease-out both',
+ blurOutHeightSlow: 'blurOut 500ms both, heightOut 500ms 300ms ease-out both',
enterFromLeft: 'enterFromLeft 300ms cubic-bezier(0.83, 0, 0.17, 1) both',
enterFromRight: 'enterFromRight 300ms cubic-bezier(0.83, 0, 0.17, 1) both',
exitToLeft: 'exitToLeft 300ms cubic-bezier(0.83, 0, 0.17, 1) both',
exitToRight: 'exitToRight 300ms cubic-bezier(0.83, 0, 0.17, 1) both',
- heightIn: 'heightIn 200ms ease both',
+ heightIn: 'heightIn 500ms ease both',
+ heightOut: 'heightOut 500ms ease both',
crawl: 'crawl 2s ease-in-out infinite',
},
keyframes: {
@@ -494,28 +500,28 @@ const config: Config = {
to: { opacity: '0', transform: 'rotateX(-10deg) scale(0.95)' },
},
blurIn: {
- from: { filter: 'blur(6px)', opacity: '0', transform: 'scale(0.95)' },
- to: { filter: 'blur(0px)', opacity: '1', transform: 'scale(1)' },
+ '0%': { filter: 'blur(6px)', opacity: '0', transform: 'scale(0.95)' },
+ '100%': { filter: 'blur(0px)', opacity: '1', transform: 'scale(1)' },
},
blurOut: {
- from: { filter: 'blur(0px)', opacity: '1', transform: 'scale(1)' },
- to: { filter: 'blur(6px)', opacity: '0', transform: 'scale(0.95)' },
+ '0%': { filter: 'blur(0px)', opacity: '1', transform: 'scale(1)' },
+ '100%': { filter: 'blur(6px)', opacity: '0', transform: 'scale(0.95)' },
},
blurInDisplay: {
- from: {
+ '0%': {
filter: 'blur(6px)',
opacity: '0',
transform: 'scale(0.95)',
display: 'none',
},
- to: {
+ '100%': {
filter: 'blur(0px)',
opacity: '1',
transform: 'scale(1)',
},
},
blurOutDisplay: {
- from: {
+ '0%': {
filter: 'blur(0px)',
opacity: '1',
transform: 'scale(1)',
@@ -534,16 +540,25 @@ const config: Config = {
heightIn: {
from: {
height: '0',
- opacity: '0',
+ paddingTop: '0',
+ paddingBottom: '0',
+ marginTop: '0',
+ marginBottom: '0',
+ gap: '0',
+ overflow: 'hidden',
+ },
+ to: { height: 'max-content' },
+ },
+ heightOut: {
+ from: { height: 'max-content' },
+ to: {
+ height: '0',
+ paddingTop: '0',
+ paddingBottom: '0',
marginTop: '0',
marginBottom: '0',
gap: '0',
},
- to: { height: 'max-content', opacity: '1' },
- },
- heightOut: {
- from: { height: 'max-content', opacity: '1' },
- to: { height: '0', opacity: '0', marginTop: '0', marginBottom: '0', gap: '0' },
},
crawl: {
'0%': {
@@ -607,6 +622,28 @@ const config: Config = {
'4xl': '2144px',
},
},
+ animationDuration: {
+ '100': '100ms',
+ '200': '200ms',
+ '300': '300ms',
+ '500': '500ms',
+ '1000': '1000ms',
+ '1500': '1500ms',
+ '2000': '2000ms',
+ '2500': '2500ms',
+ '3000': '3000ms',
+ },
+ animationDelay: {
+ '100': '100ms',
+ '200': '200ms',
+ '300': '300ms',
+ '500': '500ms',
+ '1000': '1000ms',
+ '1500': '1500ms',
+ '2000': '2000ms',
+ '2500': '2500ms',
+ '3000': '3000ms',
+ },
},
opacity: opacity(),
screens: {