UNPKG

@redocly/theme

Version:

Shared UI components lib

448 lines (434 loc) 24 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SearchDialog = SearchDialog; const react_1 = __importStar(require("react")); const styled_components_1 = __importDefault(require("styled-components")); const SearchInput_1 = require("../../components/Search/SearchInput"); const SearchShortcut_1 = require("../../components/Search/SearchShortcut"); const Button_1 = require("../../components/Button/Button"); const utils_1 = require("../../core/utils"); const SearchItem_1 = require("../../components/Search/SearchItem"); const SearchRecent_1 = require("../../components/Search/SearchRecent"); const SearchSuggestedPages_1 = require("../../components/Search/SearchSuggestedPages"); const hooks_1 = require("../../core/hooks"); const Tag_1 = require("../../components/Tag/Tag"); const CloseIcon_1 = require("../../icons/CloseIcon/CloseIcon"); const SearchFilter_1 = require("../../components/Search/SearchFilter"); const SearchGroups_1 = require("../../components/Search/SearchGroups"); const Typography_1 = require("../../components/Typography/Typography"); const SpinnerLoader_1 = require("../../components/Loaders/SpinnerLoader"); const SearchAiDialog_1 = require("../../components/Search/SearchAiDialog"); const SettingsIcon_1 = require("../../icons/SettingsIcon/SettingsIcon"); const AiStarsIcon_1 = require("../../icons/AiStarsIcon/AiStarsIcon"); const ReturnKeyIcon_1 = require("../../icons/ReturnKeyIcon/ReturnKeyIcon"); const ChevronLeftIcon_1 = require("../../icons/ChevronLeftIcon/ChevronLeftIcon"); const EditIcon_1 = require("../../icons/EditIcon/EditIcon"); const AiStarsGradientIcon_1 = require("../../icons/AiStarsGradientIcon/AiStarsGradientIcon"); function SearchDialog({ onClose, className }) { const { useTranslate, useCurrentProduct, useSearch, useProducts, useAiSearch, useOtelTelemetry } = (0, hooks_1.useThemeHooks)(); const otelTelemetry = useOtelTelemetry(); const products = useProducts(); const currentProduct = useCurrentProduct(); const [product, setProduct] = (0, react_1.useState)(currentProduct); const [mode, setMode] = (0, react_1.useState)('search'); const autoSearchDisabled = mode !== 'search'; const { query, setQuery, filter, setFilter, items, isSearchLoading, facets, setLoadMore, advancedSearch, askAi, groupField, } = useSearch(product === null || product === void 0 ? void 0 : product.name, autoSearchDisabled); const { isFilterOpen, onFilterToggle, onFilterChange, onFilterReset, onFacetReset, onQuickFilterReset, } = (0, hooks_1.useSearchFilter)(filter, setFilter); const { addSearchHistoryItem } = (0, hooks_1.useRecentSearches)(); const aiSearch = useAiSearch({ filter }); const searchInputRef = (0, react_1.useRef)(null); const modalRef = (0, react_1.useRef)(null); const aiQueryRef = (0, react_1.useRef)(null); const firstSearchResultRef = (0, react_1.useRef)(null); const searchKeysWithResults = items ? Object.keys(items).filter((key) => { var _a; return (_a = items[key]) === null || _a === void 0 ? void 0 : _a.length; }) : []; const { translate } = useTranslate(); const handleClose = (0, react_1.useCallback)(() => { var _a; const value = (_a = searchInputRef === null || searchInputRef === void 0 ? void 0 : searchInputRef.current) === null || _a === void 0 ? void 0 : _a.value; if (value) { addSearchHistoryItem(value); } onClose(); }, [addSearchHistoryItem, onClose]); (0, hooks_1.useDialogHotKeys)(modalRef, handleClose); const focusSearchInput = () => { requestAnimationFrame(() => { var _a; (_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }); }; (0, react_1.useEffect)(() => { if (mode === 'ai-dialog' && aiSearch.isGeneratingResponse) { setQuery(''); } }, [mode, aiSearch.isGeneratingResponse, setQuery]); (0, react_1.useEffect)(focusSearchInput, []); const handleOverlayClick = (0, react_1.useCallback)((event) => { var _a; const target = event.target; if (typeof target.className !== 'string') return; if ((_a = target.className) === null || _a === void 0 ? void 0 : _a.includes(' overlay')) { handleClose(); } }, [handleClose]); const mapItem = (0, react_1.useCallback)((item, index, results, innerRef) => { var _a; let itemProduct; if (!product && item.document.product) { const folder = (_a = item.document.product) === null || _a === void 0 ? void 0 : _a.folder; const resolvedProduct = products.find((product) => product.slug.match(`/${folder.startsWith('./') ? folder.slice(2) : folder}/`)); itemProduct = resolvedProduct ? { name: resolvedProduct.name, icon: resolvedProduct.icon } : undefined; } return (react_1.default.createElement(SearchItem_1.SearchItem, { key: `${index}-${item.document.id}`, item: item, product: itemProduct, innerRef: innerRef, onClick: () => { addSearchHistoryItem(query); otelTelemetry.send({ type: 'search.result.clicked', payload: { query, url: item.document.url, total_results: results.length.toString(), index: index.toString(), search_engine: mode, }, }); onClose(); } })); }, [onClose, product, products, addSearchHistoryItem, query, otelTelemetry, mode]); const showLoadMore = (0, react_1.useCallback)((groupKey, currentCount = 0) => { const groupFacet = facets.find((facet) => facet.field === groupField); let needLoadMore = false; if (groupFacet) { const groupValue = groupFacet.values.find((value) => { if (typeof value === 'object') { return value.value === groupKey; } else return false; }); needLoadMore = groupValue ? groupValue.count > currentCount : false; } return needLoadMore; }, [facets, groupField]); const showResults = !!((filter && filter.length) || query); const showSearchFilterButton = advancedSearch && mode === 'search'; const showAiSearchButton = askAi && mode === 'search'; const showAiSearchItem = showAiSearchButton && query; const showHeaderButtons = showSearchFilterButton || showAiSearchButton; return (react_1.default.createElement(SearchOverlay, { "data-component-name": "Search/SearchDialog", ref: modalRef, onClick: handleOverlayClick, className: (0, utils_1.concatClassNames)('overlay', className) }, react_1.default.createElement(SearchDialogWrapper, { className: "scroll-lock", role: "dialog" }, react_1.default.createElement(SearchDialogHeader, null, product && (react_1.default.createElement(SearchProductTag, { color: "product" }, product.name, react_1.default.createElement(CloseIcon_1.CloseIcon, { onClick: () => setProduct(undefined), color: "--icon-color-additional" }))), mode === 'search' ? (react_1.default.createElement(react_1.default.Fragment, null, react_1.default.createElement(SearchInput_1.SearchInput, { value: query, onChange: setQuery, placeholder: translate('search.label', 'Search docs...'), isLoading: isSearchLoading, inputRef: searchInputRef, onSubmit: () => { var _a; if (isSearchLoading) return; if (showAiSearchButton && aiQueryRef.current) { aiQueryRef.current.focus(); } else { (_a = firstSearchResultRef.current) === null || _a === void 0 ? void 0 : _a.focus(); } }, "data-translation-key": "search.label" }), showHeaderButtons && (react_1.default.createElement(SearchHeaderButtons, null, showAiSearchButton ? (react_1.default.createElement(SearchAiButton, { icon: react_1.default.createElement(AiStarsGradientIcon_1.AiStarsGradientIcon, { color: "var(--search-ai-button-icon-color)" }), onClick: () => { setMode('ai-dialog'); if (query.trim()) { aiSearch.askQuestion(query); } } }, translate('search.ai.button', 'Search with AI'))) : null, showSearchFilterButton && (react_1.default.createElement(SearchFilterToggleButton, { icon: react_1.default.createElement(SettingsIcon_1.SettingsIcon, null), onClick: onFilterToggle })))))) : (react_1.default.createElement(AiDialogHeaderWrapper, null, react_1.default.createElement(Button_1.Button, { variant: "secondary", onClick: () => { setMode('search'); aiSearch.clearConversation(); focusSearchInput(); }, tabIndex: 0, icon: react_1.default.createElement(ChevronLeftIcon_1.ChevronLeftIcon, null) }, translate('search.ai.backToSearch', 'Back to search')), react_1.default.createElement(Button_1.Button, { variant: "secondary", disabled: !aiSearch.conversation.length, onClick: () => aiSearch.clearConversation(), tabIndex: 0, icon: react_1.default.createElement(EditIcon_1.EditIcon, null) }, translate('search.ai.newConversation', 'New conversation'))))), react_1.default.createElement(SearchDialogBody, null, mode === 'search' ? (react_1.default.createElement(react_1.default.Fragment, null, advancedSearch && isFilterOpen && (react_1.default.createElement(SearchDialogBodyFilterView, null, react_1.default.createElement(SearchFilter_1.SearchFilter, { facets: facets, filter: filter, query: query, quickFilterFields: [groupField], onFilterChange: onFilterChange, onFilterReset: onFilterReset, onFacetReset: onFacetReset }))), react_1.default.createElement(SearchDialogBodyMainView, null, react_1.default.createElement(SearchGroups_1.SearchGroups, { facets: facets, searchFilter: filter, onFilterChange: onFilterChange, onQuickFilterReset: onQuickFilterReset, groupField: groupField }), showAiSearchItem && (react_1.default.createElement(SearchWithAI, { onClick: () => { setMode('ai-dialog'); if (query.trim()) { aiSearch.askQuestion(query); } }, onKeyDown: (e) => { if (e.key === 'Enter') { setMode('ai-dialog'); if (query.trim()) { aiSearch.askQuestion(query); } } }, ref: aiQueryRef, tabIndex: 0, role: "option", "aria-selected": "true" }, react_1.default.createElement(AiStarsIcon_1.AiStarsIcon, { color: "var(--search-ai-icon-color)", size: "36px", background: "var(--search-ai-icon-bg-color)", margin: "0 var(--spacing-md) 0 0", borderRadius: "var(--border-radius-lg)" }), react_1.default.createElement(Typography_1.Typography, { fontWeight: "var(--font-weight-semibold)" }, query), react_1.default.createElement(Typography_1.Typography, null, "- ", translate('search.ai.label', 'Ask AI assistant')), react_1.default.createElement(ReturnKeyIcon_1.ReturnKeyIcon, { color: "var(--search-item-text-color)" }))), showResults ? (searchKeysWithResults.length ? (searchKeysWithResults.map((key, searchGroupKeyIdx) => { const searchResultItems = items[key]; if (searchResultItems === null || searchResultItems === void 0 ? void 0 : searchResultItems.length) { return (react_1.default.createElement(react_1.Fragment, { key: key }, react_1.default.createElement(SearchGroupTitle, { "data-testid": "search-group-title" }, key), searchResultItems.map((item, idx, resultsArr) => mapItem(item, idx, resultsArr, searchGroupKeyIdx === 0 ? firstSearchResultRef : undefined)), showLoadMore(key, searchResultItems.length || 0) && (react_1.default.createElement(SearchGroupFooter, { tabIndex: 0, "data-translation-key": "search.showMore", onKeyDown: (e) => { if (e.key === 'Enter') { setLoadMore({ groupKey: key, offset: searchResultItems.length || 0, }); } }, onClick: () => setLoadMore({ groupKey: key, offset: searchResultItems.length || 0, }) }, translate('search.showMore', 'Show more'))))); } return null; })) : isSearchLoading ? (react_1.default.createElement(SearchMessage, { "data-translation-key": "search.loading" }, react_1.default.createElement(SpinnerLoader_1.SpinnerLoader, { size: "26px", color: "var(--search-input-icon-color)" }), translate('search.loading', 'Loading...'))) : (react_1.default.createElement(SearchMessage, { "data-translation-key": "search.noResults" }, react_1.default.createElement("b", null, translate('search.noResults.title', 'No results'))))) : (react_1.default.createElement(react_1.default.Fragment, null, react_1.default.createElement(SearchRecent_1.SearchRecent, { onSelect: (query, index) => { otelTelemetry.send({ type: 'search.recent.clicked', payload: { query, index: index.toString(), }, }); setQuery(query); focusSearchInput(); } }), react_1.default.createElement(SearchSuggestedPages_1.SearchSuggestedPages, null)))))) : (react_1.default.createElement(SearchAiDialog_1.SearchAiDialog, { initialMessage: query, response: aiSearch.response, isGeneratingResponse: aiSearch.isGeneratingResponse, error: aiSearch.error, resources: aiSearch.resources, conversation: aiSearch.conversation, setConversation: aiSearch.setConversation, onMessageSent: aiSearch.askQuestion }))), react_1.default.createElement(SearchDialogFooter, null, mode === 'ai-dialog' ? (react_1.default.createElement(AiDisclaimer, null, translate('search.ai.disclaimer', 'AI search might provide incomplete or incorrect results. Verify important information.'))) : (react_1.default.createElement(react_1.default.Fragment, null, react_1.default.createElement(SearchShortcuts, null, react_1.default.createElement(SearchShortcut_1.SearchShortcut, { "data-translation-key": "search.keys.navigate", combination: "Tab", text: translate('search.keys.navigate', 'to navigate') }), react_1.default.createElement(SearchShortcut_1.SearchShortcut, { "data-translation-key": "search.keys.select", combination: "\u23CE", text: translate('search.keys.select', 'to select') }), react_1.default.createElement(SearchShortcut_1.SearchShortcut, { "data-translation-key": "search.keys.exit", combination: "Esc", text: translate('search.keys.exit', 'to exit') })), isSearchLoading && (react_1.default.createElement(SearchLoading, null, react_1.default.createElement(SpinnerLoader_1.SpinnerLoader, { size: "16px", color: "var(--search-input-icon-color)" }), translate('search.loading', 'Loading...'))), react_1.default.createElement(SearchCancelButton, { "data-translation-key": "search.cancel", variant: "secondary", size: "small", onClick: handleClose }, translate('search.cancel', 'Cancel')))))))); } const SearchOverlay = styled_components_1.default.div ` position: fixed; display: flex; align-items: center; justify-content: center; top: 0; left: 0; width: 100vw; height: 100vh; background: var(--bg-color-modal-overlay); z-index: var(--z-index-overlay); @media screen and (max-width: ${utils_1.breakpoints.small}) { align-items: start; position: fixed; overflow: hidden; overscroll-behavior: none; } `; const SearchDialogWrapper = styled_components_1.default.div ` display: flex; flex-direction: column; overflow: auto; width: 100vw; height: 100vh; background: var(--search-modal-bg-color); box-shadow: var(--search-modal-box-shadow); border-radius: 0; @media screen and (max-width: ${utils_1.breakpoints.small}) { min-height: -webkit-fill-available !important; min-height: 100dvh !important; height: auto !important; width: 100vw !important; } @media screen and (min-width: ${utils_1.breakpoints.small}) { border-radius: var(--search-modal-border-radius); width: var(--search-modal-width); min-height: var(--search-modal-min-height); min-width: var(--search-modal-min-width); max-width: 95vw; max-height: 95vh; height: var(--search-modal-min-height); resize: both; } `; const SearchDialogHeader = styled_components_1.default.header ` display: flex; align-items: center; border-bottom: var(--search-modal-border); background-color: var(--search-modal-header-bg-color); padding: var(--search-modal-header-padding); `; const AiDialogHeaderWrapper = styled_components_1.default.div ` display: flex; justify-content: space-between; align-items: center; width: 100%; `; const SearchDialogBody = styled_components_1.default.div ` display: flex; flex-direction: row-reverse; flex: 1; min-height: 0; overflow: hidden; @media screen and (max-width: ${utils_1.breakpoints.small}) { min-height: 0; } `; const SearchDialogBodyMainView = styled_components_1.default.div ` flex: 2; flex-grow: 2; overflow-y: scroll; overscroll-behavior: contain; border-right: var(--search-modal-border); `; const SearchDialogBodyFilterView = styled_components_1.default.div ` overflow: scroll; `; const SearchDialogFooter = styled_components_1.default.footer ` display: flex; gap: var(--search-modal-footer-gap); padding: var(--search-modal-footer-padding); border-top: var(--search-modal-border); `; const SearchShortcuts = styled_components_1.default.div ` display: none; justify-content: flex-start; align-items: center; gap: var(--search-shortcuts-gap); @media screen and (min-width: ${utils_1.breakpoints.small}) { display: flex; } `; const SearchMessage = styled_components_1.default.div ` display: flex; height: 40%; justify-content: center; align-items: center; flex-direction: column; font-size: var(--search-message-font-size); font-weight: var(--search-message-font-weight); line-height: var(--search-message-line-height); color: var(--search-message-text-color); gap: var(--search-message-gap); `; const SearchProductTag = (0, styled_components_1.default)(Tag_1.Tag) ` --tag-border-radius: var(--border-radius); border: none; margin: var(--spacing-xs) var(--spacing-sm) !important; `; const SearchFilterToggleButton = (0, styled_components_1.default)(Button_1.Button) ` margin-left: 0; `; const SearchAiButton = (0, styled_components_1.default)(Button_1.Button) ` margin-left: 0; `; const SearchCancelButton = (0, styled_components_1.default)(Button_1.Button) ` width: 100%; @media screen and (min-width: ${utils_1.breakpoints.small}) { display: none; } `; const SearchGroupTitle = styled_components_1.default.div ` border-bottom: var(--search-modal-border); padding: var(--search-group-title-padding); background-color: var(--search-group-title-bg-color); `; const SearchGroupFooter = styled_components_1.default.div ` display: flex; justify-content: center; padding: var(--search-group-footer-padding); color: var(--search-group-footer-text-color); cursor: pointer; `; const SearchLoading = styled_components_1.default.div ` display: none; align-items: center; gap: var(--spacing-xs); @media screen and (min-width: ${utils_1.breakpoints.small}) { display: flex; } `; const SearchHeaderButtons = styled_components_1.default.div ` display: flex; gap: var(--search-header-buttons-gap); padding-left: var(--search-header-buttons-padding-left); border-left: var(--search-header-buttons-border-left); `; const AiDisclaimer = styled_components_1.default.div ` font-size: var(--search-ai-disclaimer-font-size); line-height: var(--search-ai-disclaimer-line-height); color: var(--search-ai-disclaimer-text-color); margin: 0 auto; `; const SearchWithAI = styled_components_1.default.div ` display: flex; justify-content: flex-start; align-items: center; cursor: pointer; gap: var(--spacing-unit); padding: var(--spacing-md); color: var(--search-item-text-color); background-color: var(--search-item-bg-color); text-decoration: none; white-space: normal; outline: none; border-top: 1px solid var(--search-item-border-color); border-bottom: 1px solid var(--search-item-border-color); transition: all 0.3s ease; ${ReturnKeyIcon_1.ReturnKeyIcon} { opacity: 0; } &:focus, &:hover { color: var(--search-item-text-color-hover); background-color: var(--search-item-bg-color-hover); ${ReturnKeyIcon_1.ReturnKeyIcon} { opacity: 1; } } &:focus { border-top: 1px solid var(--search-item-border-color-focused); border-bottom: 1px solid var(--search-item-border-color-focused); } & > :first-child { margin-right: var(--spacing-xs); } `; //# sourceMappingURL=SearchDialog.js.map