commit fa42ab2bd9d8ae405c724b25b7c97050ddf7106b Author: François Chalifour Date: Tue Mar 31 17:08:28 2020 +0200 feat(docsearch): add DocSearch for Docusaurus (#39) Co-authored-by: eunjae-lee Co-authored-by: Shipow diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000..7d68e20c --- /dev/null +++ b/babel.config.js @@ -0,0 +1,27 @@ +/* eslint-disable import/no-commonjs */ + +module.exports = api => { + const isTest = api.env('test'); + const modules = isTest ? 'commonjs' : false; + const targets = {}; + + if (isTest) { + targets.node = true; + } else { + targets.browsers = ['last 2 versions', 'ie >= 9']; + } + + return { + presets: [ + '@babel/preset-typescript', + [ + '@babel/preset-env', + { + modules, + targets, + }, + ], + ], + plugins: [['@babel/plugin-transform-react-jsx']], + }; +}; diff --git a/package.json b/package.json new file mode 100644 index 00000000..d9e6bdc6 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "docsearch-react", + "version": "1.0.0-alpha.10", + "license": "MIT", + "homepage": "https://github.com/algolia/autocomplete.js", + "repository": "algolia/autocomplete.js", + "author": { + "name": "Algolia, Inc.", + "url": "https://www.algolia.com" + }, + "sideEffects": false, + "files": [ + "dist/" + ], + "source": "src/index.ts", + "types": "dist/esm/index.d.ts", + "module": "dist/esm/index.js", + "main": "dist/umd/index.js", + "umd:main": "dist/umd/index.js", + "unpkg": "dist/umd/index.js", + "jsdelivr": "dist/umd/index.js", + "scripts": { + "build": "yarn build:clean && yarn build:umd && yarn build:esm && yarn build:css", + "": "// TODO: have a proper build:css, probably including autoprefixer?", + "build:css": "cp src/style.css dist/esm/ && cp src/style.css dist/umd/", + "build:css:watch": "chokidar src/style.css --command \"yarn build:css\"", + "build:esm": "babel src --root-mode upward --extensions '.ts,.tsx' --out-dir dist/esm", + "build:esm:watch": "yarn build:esm --watch", + "build:umd": "rollup --config", + "build:types": "tsc -p ./tsconfig.declaration.json --outDir ./dist/esm", + "build:types:watch": "chokidar \"**/*.ts\" \"**/*.tsx\" --command \"yarn build:types\" --ignore \"dist\"", + "build:clean": "rm -rf ./dist", + "watch": "concurrently \"yarn build:esm:watch\" \"yarn build:types:watch\" \"yarn build:css:watch\"" + }, + "dependencies": { + "@francoischalifour/autocomplete-core": "^1.0.0-alpha.10", + "@francoischalifour/autocomplete-preset-algolia": "^1.0.0-alpha.10", + "algoliasearch": "^4.0.0" + }, + "peerDependencies": { + "react": "^16.8.0", + "react-dom": "^16.8.0" + } +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 00000000..a64b3b2b --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,20 @@ +import json from '@rollup/plugin-json'; + +import { name } from './package.json'; +import { sharedPlugins } from '../autocomplete-core/rollup.config'; + +export default { + input: 'src/index.ts', + external: ['react', 'react-dom'], + output: { + file: 'dist/umd/index.js', + format: 'umd', + sourcemap: true, + name, + globals: { + react: 'React', + 'react-dom': 'ReactDOM', + }, + }, + plugins: [json(), ...sharedPlugins], +}; diff --git a/src/DocSearch.tsx b/src/DocSearch.tsx new file mode 100644 index 00000000..c9883122 --- /dev/null +++ b/src/DocSearch.tsx @@ -0,0 +1,235 @@ +import React, { useRef, useEffect } from 'react'; +import { + createAutocomplete, + AutocompleteState, +} from '@francoischalifour/autocomplete-core'; +import { getAlgoliaHits } from '@francoischalifour/autocomplete-preset-algolia'; + +import { DocSearchHit, InternalDocSearchHit } from './types'; +import { createSearchClient, groupBy, noop } from './utils'; +import { SearchBox } from './SearchBox'; +import { Dropdown } from './Dropdown'; +import { Footer } from './Footer'; + +interface DocSearchProps { + appId?: string; + apiKey: string; + indexName: string; + searchParameters: any; + onClose(): void; +} + +export function DocSearch({ + appId = 'BH4D9OD16A', + apiKey, + indexName, + searchParameters, + onClose = noop, +}: DocSearchProps) { + const [state, setState] = React.useState< + AutocompleteState + >({ + query: '', + suggestions: [], + } as any); + + const searchBoxRef = useRef(null); + const dropdownRef = useRef(null); + const inputRef = useRef(null); + const snipetLength = useRef(10); + + const searchClient = React.useMemo(() => createSearchClient(appId, apiKey), [ + appId, + apiKey, + ]); + + const { + getEnvironmentProps, + getRootProps, + getFormProps, + getLabelProps, + getInputProps, + getMenuProps, + getItemProps, + } = React.useMemo( + () => + createAutocomplete< + InternalDocSearchHit, + React.FormEvent, + React.MouseEvent, + React.KeyboardEvent + >({ + defaultHighlightedIndex: 0, + autoFocus: true, + placeholder: 'Search docs...', + openOnFocus: true, + onStateChange({ state }) { + setState(state as any); + }, + getSources({ query }) { + return getAlgoliaHits({ + searchClient, + queries: [ + { + indexName, + query, + params: { + attributesToRetrieve: [ + 'hierarchy.lvl0', + 'hierarchy.lvl1', + 'hierarchy.lvl2', + 'hierarchy.lvl3', + 'hierarchy.lvl4', + 'hierarchy.lvl5', + 'hierarchy.lvl6', + 'content', + 'type', + 'url', + ], + attributesToSnippet: [ + `hierarchy.lvl1:${snipetLength.current}`, + `hierarchy.lvl2:${snipetLength.current}`, + `hierarchy.lvl3:${snipetLength.current}`, + `hierarchy.lvl4:${snipetLength.current}`, + `hierarchy.lvl5:${snipetLength.current}`, + `hierarchy.lvl6:${snipetLength.current}`, + `content:${snipetLength.current}`, + ], + snippetEllipsisText: '…', + highlightPreTag: '', + highlightPostTag: '', + hitsPerPage: 20, + distinct: 4, + ...searchParameters, + }, + }, + ], + }).then((hits: DocSearchHit[]) => { + const formattedHits = hits.map(hit => { + const url = new URL(hit.url); + return { + ...hit, + url: hit.url + // @TODO: temporary convenience for development. + .replace(url.origin, '') + .replace('#__docusaurus', ''), + }; + }); + const sources = groupBy(formattedHits, hit => hit.hierarchy.lvl0); + + return Object.values(sources).map(items => { + return { + onSelect() { + onClose(); + }, + getSuggestionUrl({ suggestion }) { + return suggestion.url; + }, + getSuggestions() { + return Object.values( + groupBy(items, item => item.hierarchy.lvl1) + ) + .map(hits => + hits.map(item => { + return { + ...item, + // eslint-disable-next-line @typescript-eslint/camelcase + __docsearch_parent: + item.type !== 'lvl1' && + hits.find( + siblingItem => + siblingItem.type === 'lvl1' && + siblingItem.hierarchy.lvl1 === + item.hierarchy.lvl1 + ), + }; + }) + ) + .flat(); + }, + }; + }); + }); + }, + }), + [indexName, searchParameters, searchClient, onClose] + ); + + useEffect(() => { + const isMobileMediaQuery = window.matchMedia('(max-width: 750px)'); + + if (isMobileMediaQuery.matches) { + snipetLength.current = 5; + } + }, []); + + useEffect(() => { + if (dropdownRef.current) { + dropdownRef.current.scrollTop = 0; + } + }, [state.query]); + + useEffect(() => { + if (!(searchBoxRef.current && dropdownRef.current && inputRef.current)) { + return undefined; + } + + const { onTouchStart, onTouchMove } = getEnvironmentProps({ + searchBoxElement: searchBoxRef.current, + dropdownElement: dropdownRef.current, + inputElement: inputRef.current, + }); + + window.addEventListener('touchstart', onTouchStart); + window.addEventListener('touchmove', onTouchMove); + + return () => { + window.removeEventListener('touchstart', onTouchStart); + window.removeEventListener('touchmove', onTouchMove); + }; + }, [getEnvironmentProps, searchBoxRef, dropdownRef, inputRef]); + + return ( +
+
+
+ +
+ +
+ +
+ +
+
+
+
+
+ ); +} diff --git a/src/Dropdown/Dropdown.tsx b/src/Dropdown/Dropdown.tsx new file mode 100644 index 00000000..9aefaa8a --- /dev/null +++ b/src/Dropdown/Dropdown.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { + AutocompleteState, + GetMenuProps, + GetItemProps, +} from '@francoischalifour/autocomplete-core'; + +import { InternalDocSearchHit } from '../types'; +import { Error } from '../Error'; +import { NoResults } from '../NoResults'; +import { Results } from '../Results'; + +interface DropdownProps { + state: AutocompleteState; + getMenuProps: GetMenuProps; + getItemProps: GetItemProps; +} + +export function Dropdown(props: DropdownProps) { + if (props.state.status === 'error') { + return ; + } + + if ( + props.state.status === 'idle' && + props.state.suggestions.every(source => source.items.length === 0) + ) { + return ; + } + + return ( + + ); +} diff --git a/src/Dropdown/index.ts b/src/Dropdown/index.ts new file mode 100644 index 00000000..2f29bad4 --- /dev/null +++ b/src/Dropdown/index.ts @@ -0,0 +1 @@ +export * from './Dropdown'; diff --git a/src/Error/Error.tsx b/src/Error/Error.tsx new file mode 100644 index 00000000..12d983b2 --- /dev/null +++ b/src/Error/Error.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export function Error() { + return ( +

+ We‘re unable to fetch results. You might want to check your network + connection. +

+ ); +} diff --git a/src/Error/index.ts b/src/Error/index.ts new file mode 100644 index 00000000..ae6e95d0 --- /dev/null +++ b/src/Error/index.ts @@ -0,0 +1 @@ +export * from './Error'; diff --git a/src/Footer/AlgoliaLogo.tsx b/src/Footer/AlgoliaLogo.tsx new file mode 100644 index 00000000..7690b5f7 --- /dev/null +++ b/src/Footer/AlgoliaLogo.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +export function AlgoliaLogo() { + return ( + + Search by + + + + + ); +} diff --git a/src/Footer/Footer.tsx b/src/Footer/Footer.tsx new file mode 100644 index 00000000..7e098632 --- /dev/null +++ b/src/Footer/Footer.tsx @@ -0,0 +1,64 @@ +import React from 'react'; + +import { AlgoliaLogo } from './AlgoliaLogo'; + +export function Footer() { + return ( + <> +
+ +
+
    +
  • + + + + + + to select +
  • +
  • + + + + + + + + + + + to navigate +
  • +
  • + + + + + + to close +
  • +
+ + ); +} + +interface CommandIconProps { + children: React.ReactNode; +} + +function CommandIcon(props: CommandIconProps) { + return ( + + + {props.children} + + + ); +} diff --git a/src/Footer/index.ts b/src/Footer/index.ts new file mode 100644 index 00000000..ddcc5a9c --- /dev/null +++ b/src/Footer/index.ts @@ -0,0 +1 @@ +export * from './Footer'; diff --git a/src/Footer/index.tsx b/src/Footer/index.tsx new file mode 100644 index 00000000..ddcc5a9c --- /dev/null +++ b/src/Footer/index.tsx @@ -0,0 +1 @@ +export * from './Footer'; diff --git a/src/NoResults/NoResults.tsx b/src/NoResults/NoResults.tsx new file mode 100644 index 00000000..3c2793e3 --- /dev/null +++ b/src/NoResults/NoResults.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +interface NoResultsProps { + query: string; +} + +export function NoResults(props: NoResultsProps) { + return
+
No results for “{props.query}“.
+
Try another search or if you believe this query should lead to actual results, please let us know with a GitHub issue.
+
; +} diff --git a/src/NoResults/index.ts b/src/NoResults/index.ts new file mode 100644 index 00000000..20eae4db --- /dev/null +++ b/src/NoResults/index.ts @@ -0,0 +1 @@ +export * from './NoResults'; diff --git a/src/Results/ActionIcon.tsx b/src/Results/ActionIcon.tsx new file mode 100644 index 00000000..15e955e0 --- /dev/null +++ b/src/Results/ActionIcon.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +export function SelectIcon() { + return ( + + + + + + + ); +} + +export function GoToExternal() { + return ( + + + + ); +} diff --git a/src/Results/Results.tsx b/src/Results/Results.tsx new file mode 100644 index 00000000..def174b4 --- /dev/null +++ b/src/Results/Results.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { + GetMenuProps, + GetItemProps, + AutocompleteSuggestion, +} from '@francoischalifour/autocomplete-core'; + +import { SourceIcon } from './SourceIcon'; +import { SelectIcon } from './ActionIcon'; +import { InternalDocSearchHit } from '../types'; + +interface ResultsProps { + suggestions: Array>; + getMenuProps: GetMenuProps; + getItemProps: GetItemProps; +} + +export function Results(props: ResultsProps) { + return ( +
+ {props.suggestions.map(({ source, items }) => { + const title = items[0].hierarchy.lvl0; + + return ( +
+
{title}
+ + +
+ ); + })} +
+ ); +} diff --git a/src/Results/SourceIcon.tsx b/src/Results/SourceIcon.tsx new file mode 100644 index 00000000..864826d3 --- /dev/null +++ b/src/Results/SourceIcon.tsx @@ -0,0 +1,58 @@ +import React from 'react'; + +export function SourceIcon(props: { type: string }) { + switch (props.type) { + case 'lvl1': + return ; + case 'content': + return ; + default: + return ; + } +} + +function LvlIcon() { + return ( + + + + ); +} + +function AnchorIcon() { + return ( + + + + ); +} + +function ContentIcon() { + return ( + + + + ); +} diff --git a/src/Results/index.ts b/src/Results/index.ts new file mode 100644 index 00000000..9646dd49 --- /dev/null +++ b/src/Results/index.ts @@ -0,0 +1 @@ +export * from './Results'; diff --git a/src/SearchBox/LoadingIcon.tsx b/src/SearchBox/LoadingIcon.tsx new file mode 100644 index 00000000..6963ca43 --- /dev/null +++ b/src/SearchBox/LoadingIcon.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +export function LoadingIcon() { + return ( + + + + + + + + + + + ); +} diff --git a/src/SearchBox/ResetIcon.tsx b/src/SearchBox/ResetIcon.tsx new file mode 100644 index 00000000..202c2cac --- /dev/null +++ b/src/SearchBox/ResetIcon.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export function ResetIcon() { + return ( + + + + ); +} diff --git a/src/SearchBox/SearchBox.tsx b/src/SearchBox/SearchBox.tsx new file mode 100644 index 00000000..8952d4ab --- /dev/null +++ b/src/SearchBox/SearchBox.tsx @@ -0,0 +1,74 @@ +import React, { MutableRefObject } from 'react'; +import { + GetFormProps, + GetLabelProps, + GetInputProps, +} from '@francoischalifour/autocomplete-core'; + +import { SearchIcon } from './SearchIcon'; +import { ResetIcon } from './ResetIcon'; +import { LoadingIcon } from './LoadingIcon'; + +interface SearchBoxProps { + inputRef: MutableRefObject; + query: string; + getFormProps: GetFormProps; + getLabelProps: GetLabelProps; + getInputProps: GetInputProps< + React.ChangeEvent, + React.MouseEvent, + React.KeyboardEvent + >; + onClose(): void; +} + +export function SearchBox(props: SearchBoxProps) { + const { onSubmit, onReset } = props.getFormProps({ + inputElement: props.inputRef.current, + }); + + return ( + <> +
+ + +
+ +
+ + + + +
+ + + + ); +} diff --git a/src/SearchBox/SearchIcon.tsx b/src/SearchBox/SearchIcon.tsx new file mode 100644 index 00000000..fab22ae7 --- /dev/null +++ b/src/SearchBox/SearchIcon.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +export function SearchIcon() { + return ( + + + + ); +} diff --git a/src/SearchBox/index.tsx b/src/SearchBox/index.tsx new file mode 100644 index 00000000..ffc46c99 --- /dev/null +++ b/src/SearchBox/index.tsx @@ -0,0 +1 @@ +export * from './SearchBox'; diff --git a/src/SearchButton/SearchButton.tsx b/src/SearchButton/SearchButton.tsx new file mode 100644 index 00000000..ddc8adf8 --- /dev/null +++ b/src/SearchButton/SearchButton.tsx @@ -0,0 +1,57 @@ +import React, { useState, useEffect } from 'react'; + +interface SearchButtonProps { + onClick(): void; +} + +const ACTION_KEY_DEFAULT = 'Ctrl'; +const ACTION_KEY_APPLE = '⌘'; + +function isAppleDevice() { + if (typeof navigator === 'undefined') { + return ACTION_KEY_DEFAULT; + } + + return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform); +} + +export function SearchButton(props: SearchButtonProps) { + const [key, setKey] = useState(() => + isAppleDevice() ? ACTION_KEY_APPLE : ACTION_KEY_DEFAULT + ); + + useEffect(() => { + if (isAppleDevice()) { + setKey(ACTION_KEY_APPLE); + } + }, []); + + return ( + + ); +} diff --git a/src/SearchButton/index.ts b/src/SearchButton/index.ts new file mode 100644 index 00000000..6f4ca098 --- /dev/null +++ b/src/SearchButton/index.ts @@ -0,0 +1 @@ +export * from './SearchButton'; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..c305abfc --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +export * from './DocSearch'; +export * from './SearchButton'; diff --git a/src/style.css b/src/style.css new file mode 100644 index 00000000..8ae3e3e8 --- /dev/null +++ b/src/style.css @@ -0,0 +1,543 @@ +/* Variables */ + +/* primary +secondary +bg +bg light +bg dark +text default +text muted +spacing +font size */ + +:root { + --docsearch-input-color: var(--ifm-color-emphasis-800); + --docsearch-highlight-color: var(--ifm-color-primary); + --docsearch-placeholder-color: rgb(150, 155, 175); + --docsearch-container-background: rgb(60, 65, 85, 0.8); + --docsearch-modal-background: var(--ifm-color-emphasis-100); + --docsearch-modal-shadow: inset 1px 1px 0px 0px rgba(255, 255, 255, 0.5), + 0px 3px 8px 0px rgba(85, 90, 100, 1); + --docsearch-searchbox-background: rgb(210, 215, 225); + --docsearch-searchbox-active-background: rgb(195, 200, 220); + --docsearch-searchbox-shadow: inset 1px 2px 6px 0px rgb(190, 195, 215), + inset 0px 1px 1px 0px rgb(134, 146, 221), 1px 1px 0px 0px white; + --docsearch-hit-color: var(--ifm-color-emphasis-800); + --docsearch-hit-active-color: white; + --docsearch-hit-background: white; + --docsearch-hit-shadow: 0px 2px 6px 0px rgb(212, 217, 225); + --docsearch-key-gradient: linear-gradient( + -225deg, + rgb(213, 219, 228) 0%, + rgb(248, 248, 248) 100% + ); + --docsearch-key-shadow: inset 0px -2px 0px 0px rgb(205, 205, 230), + inset 0px 0px 1px 1px white, 0px 1px 2px 1px rgba(30, 35, 90, 0.4); + --docsearch-footer-background: white; + --docsearch-footer-shadow: 0px -5px 7px 0px rgba(69, 98, 155, 0.12); + --docsearch-logo-color: #5468ff; + --docsearch-muted-color: rgb(190, 195, 215); + --docsearch-modal-width: 560px; + --docsearch-modal-height: 600px; + --docsearch-searchbox-height: 56px; + --docsearch-hit-height: 56px; + --docsearch-footer-height: 44px; + --docsearch-spacing: 12px; +} + +/* Darkmode */ + +html[data-theme='dark'] { + --docsearch-container-background: rgba(9, 10, 17, 0.8); + --docsearch-modal-background: rgb(21, 23, 42); + --docsearch-modal-shadow: inset 1px 1px 0px 0px rgb(44, 46, 64), + 0px 3px 8px 0px rgb(0, 3, 9); + --docsearch-searchbox-background: rgb(9, 10, 17); + --docsearch-searchbox-shadow: inset 0px 0px 0 2px var(--ifm-color-primary); + --docsearch-searchbox-active-background: rgb(9, 10, 17); + --docsearch-hit-color: var(--ifm-color-emphasis-500); + --docsearch-hit-shadow: none; + --docsearch-hit-background: rgb(9, 10, 17); + --docsearch-key-gradient: linear-gradient( + -26.56505117707799deg, + rgb(86, 88, 114) 0%, + rgb(49, 53, 91) 100% + ); + --docsearch-key-shadow: inset 0px -2px 0px 0px rgb(40, 45, 85), + inset 0px 0px 1px 1px rgb(81, 87, 125), 0px 2px 2px 0px rgb(3, 4, 9); + --docsearch-footer-background: rgb(30, 33, 54); + --docsearch-footer-shadow: inset 0px 1px 0px 0px rgba(73, 76, 106, 0.52), + 0px -4px 8px 0px rgba(0, 0, 0, 0.34); + --docsearch-logo-color: #fff; + --docsearch-muted-color: rgb(100, 105, 125); +} + +.DocSearch--active .main-wrapper { + filter: blur(2px); +} + +/* Search Button */ + +.DocSearch-SearchButton { + display: flex; + height: 40px; + margin: 0 0 0 20px; + padding: 0 12px; + margin-left: 2em; + border: none; + border-radius: 20px; + cursor: pointer; + align-items: center; + background: var(--docsearch-searchbox-background); + color: var(--ifm-color-emphasis-900); + transition: background-color 0.4s ease-out, box-shadow 0.4s ease-out; +} + +.DocSearch-SearchButton:hover, +.DocSearch-SearchButton:active, +.DocSearch-SearchButton:focus { + outline: none; + box-shadow: var(--docsearch-searchbox-shadow); + background: var(--docsearch-searchbox-active-background); +} + +.DocSearch-SearchButton-Icon { + color: var(--docsearch-highlight-color); +} + +.DocSearch-SearchButton-Placeholder { + margin: 0 8px; + font-size: 1rem; +} + +.DocSearch-SearchButton-Key { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 18px; + margin-right: 0.4em; + padding-bottom: 2px; + border-radius: 3px; + background: var(--docsearch-key-gradient); + box-shadow: var(--docsearch-key-shadow); + color: var(--ifm-color-emphasis-600); +} + +/* Body modifier */ + +.DocSearch--active { + /* + * We need to mark it as important because some websites override the + * `style` attribute (e.g. Docusaurus). + */ + overflow: hidden !important; +} + +/* Hide Docusaurus hamburger button when modal is opened */ +.DocSearch--active .menu__button { + display: none !important; +} + +/* Container & Modal */ + +.DocSearch-Container, +.DocSearch-Container * { + box-sizing: border-box; +} + +.DocSearch-Container { + height: 100vh; + width: 100vw; + position: fixed; + left: 0; + top: 0; + background-color: var(--docsearch-container-background); + animation: container 0.1s ease-in forwards; +} + +@keyframes container { + 0% { + opacity: 0.5; + } + 100% { + opacity: 1; + } +} + +.DocSearch-Container a { + text-decoration: none; +} + +.DocSearch-Modal { + position: relative; + flex-direction: column; + max-width: var(--docsearch-modal-width); + margin: 60px auto auto auto; + border-radius: 6px; + background: var(--docsearch-modal-background); + box-shadow: var(--docsearch-modal-shadow); +} + +/* Modal Searchbox */ + +.DocSearch-SearchBar { + display: flex; + padding: var(--docsearch-spacing) var(--docsearch-spacing) 0; +} + +.DocSearch-Form { + display: flex; + width: 100%; + position: relative; + height: var(--docsearch-searchbox-height); + padding: 0 var(--docsearch-spacing); + align-items: center; + border-radius: 4px; + background: var(--docsearch-searchbox-background); + box-shadow: var(--docsearch-searchbox-shadow); +} + +.DocSearch-Input { + height: 100%; + flex: 1; + width: 80%; + padding-left: 8px; + font: inherit; + font-size: 1.2em; + appearance: none; + border: none; + outline: none; + background: transparent; + color: var(--docsearch-input-color); +} + +.DocSearch-Input::placeholder { + color: var(--docsearch-placeholder-color); +} + +.DocSearch-Input::-webkit-search-cancel-button, +.DocSearch-Input::-webkit-search-decoration, +.DocSearch-Input::-webkit-search-results-button, +.DocSearch-Input::-webkit-search-results-decoration { + display: none; +} + +.DocSearch-LoadingIndicator, +.DocSearch-MagnifierLabel, +.DocSearch-Reset { + height: 20px; + width: 20px; + margin: 0; + padding: 0; +} + +.DocSearch-Container--Stalled .DocSearch-LoadingIndicator, +.DocSearch-MagnifierLabel, +.DocSearch-Reset { + display: flex; + align-items: center; + justify-content: center; +} + +.DocSearch-Container--Stalled .DocSearch-MagnifierLabel, +.DocSearch-LoadingIndicator { + display: none; +} + +.DocSearch-Reset { + appearance: none; + right: 0; + border: none; + border-radius: 50%; + background: none; + cursor: pointer; +} + +.DocSearch-Reset[hidden] { + display: none; +} + +.DocSearch-Reset:focus { + outline: none; +} + +.DocSearch-Reset svg { + height: 10px; + width: 10px; +} + +.DocSearch-LoadingIndicator svg, +.DocSearch-MagnifierLabel svg { + height: 24px; + width: 24px; +} + +.DocSearch-MagnifierLabel { + color: var(--docsearch-highlight-color); +} + +.DocSearch-Cancel { + display: none; +} + +/* Modal Dropdown */ + +.DocSearch-Dropdown { + height: calc( + var(--docsearch-modal-height) - var(--docsearch-searchbox-height) - + var(--docsearch-spacing) * 2 - var(--docsearch-footer-height) + ); + padding: 0 var(--docsearch-spacing) var(--docsearch-hit-height); + margin-bottom: var(--docsearch-footer-height); + overflow-y: scroll; + letter-spacing: 0.02em; +} + +.DocSearch-Dropdown ul { + margin: 0; + padding: 0; + list-style: none; +} + +.DocSearch-Dropdown-Container { + position: relative; +} + +/* Modal Footer */ + +.DocSearch-Footer { + position: absolute; + bottom: 0; + display: flex; + width: 100%; + height: var(--docsearch-footer-height); + padding: 0 var(--docsearch-spacing); + flex-direction: row-reverse; + flex-shrink: 0; + justify-content: space-between; + align-items: center; + border-radius: 0 0 8px 8px; + background: var(--docsearch-footer-background); + box-shadow: var(--docsearch-footer-shadow); + z-index: var(--ifm-z-index-fixed); +} + +.DocSearch-Commands { + display: flex; + margin: 0; + padding: 0; + list-style: none; +} + +.DocSearch-Commands li:not(:last-of-type) { + margin-right: 0.8em; +} + +.DocSearch-Commands { + display: flex; + color: var(--ifm-color-emphasis-600); +} + +.DocSearch-Commands li { + display: flex; + align-items: center; +} + +.DocSearch-Commands-Key { + display: flex; + width: 20px; + height: 18px; + margin-right: 0.4em; + padding-bottom: 2px; + align-items: center; + justify-content: center; + border-radius: 2px; + background: var(--docsearch-key-gradient); + box-shadow: var(--docsearch-key-shadow); +} + +.DocSearch-Logo a { + display: flex; +} + +.DocSearch-Logo svg { + margin-left: 8px; + color: var(--docsearch-logo-color); +} + +.DocSearch-Label { + font-size: 0.75em; + line-height: 1.8em; + color: var(--docsearch-muted-color); +} + +/* Hit */ + +.DocSearch-Hits mark { + background: none; + color: var(--docsearch-highlight-color); +} + +.DocSearch-Hit { + display: flex; + position: relative; + border-radius: 4px; + background: var(--docsearch-hit-background); + box-shadow: var(--docsearch-hit-shadow); + padding-left: var(--docsearch-spacing); + margin-bottom: 4px; +} + +.DocSearch-Hit-source { + margin: 0 calc(var(--docsearch-spacing) * -1); + padding: 8px var(--docsearch-spacing) 0; + line-height: 32px; + font-size: 0.85em; + font-weight: 600; + color: var(--docsearch-highlight-color); + letter-spacing: 0; + position: sticky; + top: 0; + z-index: var(--ifm-z-index-fixed); + background: var(--docsearch-modal-background); +} + +.DocSearch-Hit-Tree { + width: 24px; + height: var(--docsearch-hit-height); + margin-right: 4px; + color: var(--docsearch-muted-color); + opacity: 0.5; +} + +.DocSearch-Hit[aria-selected='true'] { + /* transition: background-color ease 0.1s; */ + background-color: var(--docsearch-highlight-color); +} + +.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-title, +.DocSearch-Hit[aria-selected='true'] mark, +.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-text, +.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-path, +.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-icon, +.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-action { + color: var(--docsearch-hit-active-color) !important; +} + +.DocSearch-Hit a { + display: block; + border-radius: 4px; + width: 100%; +} + +.DocSearch-Hit[aria-selected='true'] mark { + text-decoration: underline; +} + +.DocSearch-Hit-Container { + display: flex; + height: var(--docsearch-hit-height); + padding: 0 var(--docsearch-spacing) 0 0; + flex-direction: row; + align-items: center; + color: var(--docsearch-hit-color); +} + +.DocSearch-Hit-icon, +.DocSearch-Hit-action { + height: 20px; + width: 20px; + color: var(--docsearch-muted-color); +} + +.DocSearch-Hit-content-wrapper { + position: relative; + display: flex; + width: 80%; + margin: 0 8px; + line-height: 1.1em; + flex: 1 0 auto; + font-weight: 500; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + justify-content: center; + flex-direction: column; +} + +.DocSearch-Hit-title { + font-size: 0.9em; +} + +.DocSearch-Hit-path { + font-size: 0.7em; + color: var(--docsearch-muted-color); +} + +/* No Results */ + +.DocSearch-NoResults { + width: 80%; + margin: 0 auto; + text-align: center; + padding: var(--docsearch-spacing) 0; + margin-top: var(--docsearch-spacing); +} + +/* Responsive */ + +@media (max-width: 750px) { + :root { + /* quickfix https://stackoverflow.com/questions/37112218/css3-100vh-not-constant-in-mobile-browser */ + --docsearch-modal-height: calc(100vh - 110px); + --docsearch-spacing: 10px; + --docsearch-footer-height: 40px; + } + .DocSearch-SearchButton-KeySeparator, + .DocSearch-SearchButton-Key { + display: none; + } + .DocSearch-Modal { + border-radius: 0; + box-shadow: none; + margin: 0 auto; + } + .DocSearch-Cancel { + display: inline-block; + width: 0; + white-space: nowrap; + overflow: hidden; + margin-left: var(--docsearch-spacing); + padding: 0; + appearance: none; + border: 0; + cursor: pointer; + background: none; + flex: none; + font: inherit; + font-size: 1em; + font-weight: 500; + letter-spacing: 0.02em; + color: var(--docsearch-highlight-color); + animation: cancel-button ease-out 0.2s forwards; + } + + @keyframes cancel-button { + 100% { + width: 4em; + } + } + + .DocSearch-Commands { + display: none; + } + .DocSearch-Hit { + letter-spacing: 0; + } +} + +/* todo: hide keyboard shortcut on smartphones, touchscreens device - cross browser limitation with media q, maybe js is better https://codepen.io/Ferie/pen/vQOMmO?editors=1010 */ +@media (hover: none) and (pointer: coarse) { + /* ... */ +} diff --git a/src/svg/icon_action_external.svg b/src/svg/icon_action_external.svg new file mode 100644 index 00000000..24adf7f8 --- /dev/null +++ b/src/svg/icon_action_external.svg @@ -0,0 +1,11 @@ + + + + 1c947b1e-eb2b-4ad3-ac34-75998ca8847a@1.00x + Created with sketchtool. + + + + + + \ No newline at end of file diff --git a/src/svg/icon_action_goto.svg b/src/svg/icon_action_goto.svg new file mode 100644 index 00000000..5bcb44e5 --- /dev/null +++ b/src/svg/icon_action_goto.svg @@ -0,0 +1,14 @@ + + + + 1c20c2f5-d737-495d-a54b-64fd79363595@1.00x + Created with sketchtool. + + + + + + + + + \ No newline at end of file diff --git a/src/svg/icon_source_anchor.svg b/src/svg/icon_source_anchor.svg new file mode 100644 index 00000000..cf0ba862 --- /dev/null +++ b/src/svg/icon_source_anchor.svg @@ -0,0 +1,14 @@ + + + + 78adcbdb-966a-4cd6-9309-109b3e7780e1@1.00x + Created with sketchtool. + + + + + + + + + \ No newline at end of file diff --git a/src/svg/icon_source_content.svg b/src/svg/icon_source_content.svg new file mode 100644 index 00000000..42747459 --- /dev/null +++ b/src/svg/icon_source_content.svg @@ -0,0 +1,13 @@ + + + + 90955cc9-f7b3-4479-898c-19510cc4b6d4@1.00x + Created with sketchtool. + + + + + + + + \ No newline at end of file diff --git a/src/svg/icon_source_page.svg b/src/svg/icon_source_page.svg new file mode 100644 index 00000000..dbec55d0 --- /dev/null +++ b/src/svg/icon_source_page.svg @@ -0,0 +1,11 @@ + + + + 1f96940f-ca50-4e02-84bd-79bc02579d53@1.00x + Created with sketchtool. + + + + + + \ No newline at end of file diff --git a/src/svg/icon_source_recent.svg b/src/svg/icon_source_recent.svg new file mode 100644 index 00000000..8e16b686 --- /dev/null +++ b/src/svg/icon_source_recent.svg @@ -0,0 +1,16 @@ + + + + ee397e5b-5117-4da7-bdc8-0d42da41349d@1.00x + Created with sketchtool. + + + + + + + + + + + \ No newline at end of file diff --git a/src/svg/icon_tree_child.svg b/src/svg/icon_tree_child.svg new file mode 100644 index 00000000..c6951fca --- /dev/null +++ b/src/svg/icon_tree_child.svg @@ -0,0 +1,12 @@ + + + + 8d4bc779-eae6-4075-8edb-f964556f9cd5@1.00x + Created with sketchtool. + + + + + + + \ No newline at end of file diff --git a/src/svg/icon_tree_last-child.svg b/src/svg/icon_tree_last-child.svg new file mode 100644 index 00000000..6f9a4b2f --- /dev/null +++ b/src/svg/icon_tree_last-child.svg @@ -0,0 +1,12 @@ + + + + 658830a4-2f41-4e2b-b15f-517e3365664b@1.00x + Created with sketchtool. + + + + + + + \ No newline at end of file diff --git a/src/svg/light_footer_key_arrow-down.svg b/src/svg/light_footer_key_arrow-down.svg new file mode 100644 index 00000000..7463f23d --- /dev/null +++ b/src/svg/light_footer_key_arrow-down.svg @@ -0,0 +1,12 @@ + + + + ba2c8f8b-aab6-4715-b010-a3c910a1a4ea@1.00x + Created with sketchtool. + + + + + + + \ No newline at end of file diff --git a/src/svg/light_footer_key_arrow-up.svg b/src/svg/light_footer_key_arrow-up.svg new file mode 100644 index 00000000..1c931049 --- /dev/null +++ b/src/svg/light_footer_key_arrow-up.svg @@ -0,0 +1,12 @@ + + + + 67191d53-11cf-4c86-9c2e-8580c0fd5d08@1.00x + Created with sketchtool. + + + + + + + \ No newline at end of file diff --git a/src/svg/light_footer_key_enter.svg b/src/svg/light_footer_key_enter.svg new file mode 100644 index 00000000..85226e53 --- /dev/null +++ b/src/svg/light_footer_key_enter.svg @@ -0,0 +1,14 @@ + + + + f879c873-9cca-4202-b665-1ec6b93bb126@1.00x + Created with sketchtool. + + + + + + + + + \ No newline at end of file diff --git a/src/svg/light_footer_key_esc.svg b/src/svg/light_footer_key_esc.svg new file mode 100644 index 00000000..f693037c --- /dev/null +++ b/src/svg/light_footer_key_esc.svg @@ -0,0 +1,11 @@ + + + + 67ddc381-274d-431d-a5d3-cf7d76b0c2cd@1.00x + Created with sketchtool. + + + + + + \ No newline at end of file diff --git a/src/svg/logo-alogia.svg b/src/svg/logo-alogia.svg new file mode 100644 index 00000000..84502dc6 --- /dev/null +++ b/src/svg/logo-alogia.svg @@ -0,0 +1,11 @@ + + + + EE8C1461-B989-46C5-8B49-7EEB61DCD522@1.00x + Created with sketchtool. + + + + + + \ No newline at end of file diff --git a/src/types/DocSearchHit.ts b/src/types/DocSearchHit.ts new file mode 100644 index 00000000..71be3c93 --- /dev/null +++ b/src/types/DocSearchHit.ts @@ -0,0 +1,81 @@ +type ContentType = + | 'content' + | 'lvl0' + | 'lvl1' + | 'lvl2' + | 'lvl3' + | 'lvl4' + | 'lvl5' + | 'lvl6'; + +interface DocSearchHitAttributeHighlightResult { + value: string; + matchLevel: 'none' | 'partial' | 'full'; + matchedWords: string[]; + fullyHighlighted?: boolean; +} + +interface DocSearchHitHighlightResultHierarchy { + lvl0: DocSearchHitAttributeHighlightResult; + lvl1: DocSearchHitAttributeHighlightResult; + lvl2: DocSearchHitAttributeHighlightResult; + lvl3: DocSearchHitAttributeHighlightResult; + lvl4: DocSearchHitAttributeHighlightResult; + lvl5: DocSearchHitAttributeHighlightResult; + lvl6: DocSearchHitAttributeHighlightResult; +} + +interface DocSearchHitHighlightResult { + content: DocSearchHitAttributeHighlightResult; + hierarchy: DocSearchHitHighlightResultHierarchy; + hierarchy_camel: DocSearchHitHighlightResultHierarchy[]; +} + +interface DocSearchHitAttributeSnippetResult { + value: string; + matchLevel: 'none' | 'partial' | 'full'; +} + +interface DocSearchHitSnippetResult { + content: DocSearchHitAttributeSnippetResult; + hierarchy: DocSearchHitHighlightResultHierarchy; + hierarchy_camel: DocSearchHitHighlightResultHierarchy[]; +} + +export interface DocSearchHit { + objectID: string; + content: string | null; + url: string; + url_without_anchor: string; + type: ContentType; + anchor: string | null; + hierarchy: { + lvl0: string; + lvl1: string; + lvl2: string | null; + lvl3: string | null; + lvl4: string | null; + lvl5: string | null; + lvl6: string | null; + }; + _highlightResult: DocSearchHitHighlightResult; + _snippetResult: DocSearchHitSnippetResult; + _rankingInfo?: { + promoted: boolean; + nbTypos: number; + firstMatchedWord: number; + proximityDistance?: number; + geoDistance: number; + geoPrecision?: number; + nbExactWords: number; + words: number; + filters: number; + userScore: number; + matchedGeoLocation?: { + lat: number; + lng: number; + distance: number; + }; + }; + _distinctSeqID?: number; +} diff --git a/src/types/InternalDocSearchHit.tsx b/src/types/InternalDocSearchHit.tsx new file mode 100644 index 00000000..bf518ce9 --- /dev/null +++ b/src/types/InternalDocSearchHit.tsx @@ -0,0 +1,5 @@ +import { DocSearchHit } from './DocSearchHit'; + +export type InternalDocSearchHit = DocSearchHit & { + __docsearch_parent: null | DocSearchHit; +}; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 00000000..7ccf2033 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,2 @@ +export * from './DocSearchHit'; +export * from './InternalDocSearchHit'; diff --git a/src/utils/createSearchClient.ts b/src/utils/createSearchClient.ts new file mode 100644 index 00000000..439f66b6 --- /dev/null +++ b/src/utils/createSearchClient.ts @@ -0,0 +1,13 @@ +import algoliasearch from 'algoliasearch/dist/algoliasearch-lite.esm.browser'; + +import { version } from '../version'; + +export function createSearchClient(appId: string, apiKey: string) { + const searchClient = algoliasearch(appId, apiKey); + + if (typeof searchClient.addAlgoliaAgent === 'function') { + searchClient.addAlgoliaAgent(`docsearch-react (${version})`); + } + + return searchClient; +} diff --git a/src/utils/groupBy.ts b/src/utils/groupBy.ts new file mode 100644 index 00000000..5ea2731e --- /dev/null +++ b/src/utils/groupBy.ts @@ -0,0 +1,16 @@ +export function groupBy( + values: TValue[], + predicate: (value: TValue) => string +): Record { + return values.reduce>((acc, item) => { + const key = predicate(item); + + if (!acc.hasOwnProperty(key)) { + acc[key] = []; + } + + acc[key].push(item); + + return acc; + }, {}); +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 00000000..58e751ef --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from './createSearchClient'; +export * from './groupBy'; +export * from './noop'; diff --git a/src/utils/noop.ts b/src/utils/noop.ts new file mode 100644 index 00000000..cadb2cd0 --- /dev/null +++ b/src/utils/noop.ts @@ -0,0 +1 @@ +export function noop(..._args: any[]): void {} diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 00000000..03d93047 --- /dev/null +++ b/src/version.ts @@ -0,0 +1 @@ +export const version = '1.0.0-alpha.10'; diff --git a/tsconfig.declaration.json b/tsconfig.declaration.json new file mode 100644 index 00000000..9bd7baf2 --- /dev/null +++ b/tsconfig.declaration.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "emitDeclarationOnly": true + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..41716a7d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig" +}