diff --git a/.eslintrc.js b/.eslintrc.js index f52a89f63..81367f4ad 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -24,7 +24,10 @@ module.exports = { 'algolia/func-style-toplevel': 'error', 'no-console': 'off', + 'no-continue': 'off', + 'no-loop-func': 'off', 'consistent-return': 'off', + '@typescript-eslint/no-unused-vars': 'warn', '@typescript-eslint/explicit-member-accessibility': [ 'error', diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index 0bd9d0482..aeaeae3d7 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -77,23 +77,21 @@ You can find an HTML code snippet in the [Crawler Admin Console](https://crawler ```html - + ``` -This code automatically adds a search autocomplete widget on your website on all `` tags, using your newly created Algolia index. +This code automatically creates a new input in the specified `selector` with a ready to use autocomplete, using your newly created Algolia index. Please refer to the [full documentation](https://github.com/algolia/algoliasearch-netlify/tree/master/frontend) to configure this front-end plugin. Autocomplete preview diff --git a/docs/screenshots/frontend/dark-theme.png b/docs/screenshots/frontend/dark-theme.png new file mode 100644 index 000000000..e0851cfd7 Binary files /dev/null and b/docs/screenshots/frontend/dark-theme.png differ diff --git a/docs/screenshots/frontend/normal-theme.png b/docs/screenshots/frontend/normal-theme.png new file mode 100644 index 000000000..1e2fb288c Binary files /dev/null and b/docs/screenshots/frontend/normal-theme.png differ diff --git a/frontend/CONTRIBUTING.md b/frontend/CONTRIBUTING.md new file mode 100644 index 000000000..b6c79db6a --- /dev/null +++ b/frontend/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# Contributing + +## Scripts + +- `yarn dev`: run dev environment +- `yarn release`: build & publish the library + +## Development + +From this folder: + +```sh +yarn dev +``` + +Or from the root of the repository: + +```sh +yarn dev:frontend +``` + +This runs a `webpack-dev-server` on port 9100. +Meant to be used in conjunction with the [test website](../public/). diff --git a/frontend/README.md b/frontend/README.md index 7baa394b1..54a5069ff 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -2,26 +2,28 @@ `algoliasearch-netlify-frontend` is the front-end bundle we recommend to use with our Netlify plugin. It's designed to be compatible with the index structure extracted by the [plugin](../plugin). -It enhances existing search inputs in your website with an autocomplete menu providing search as you type results. +It **creates a new search input** in your website with an autocomplete menu providing search as you type results. ## Usage ```html - - + + ``` +

+ Frontend plugin light theme +

+ ## Available options Here's the full list of options with their default value. @@ -33,17 +35,22 @@ algoliasearchNetlify({ apiKey: '', // Search api key (Can be found in https://www.algolia.com/api-keys) siteId: '', // Netlify Site ID (Can be found in https://crawler.algolia.com/admin/netlify) branch: '', // Target git branch, either a fixed one (e.g. 'master') or a dynamic one using `process.env.HEAD`. See "Using Multiple branches" in this doc. + selector: 'div#search', // Where the autocomplete will be spawned (should not be an input) // Optional analytics: true, // Enable search analytics - autocomplete: { - hitsPerPage: 5, // Amount of results to display - inputSelector: 'input[type=search]', // CSS selector of your search input(s) - }, - color: '#3c4fe0', // Main color - debug: false, // Debug mode (keeps the autocomplete open) - silenceWarnings: false, // Disable warnings (e.g. no search input found) + hitsPerPage: 5, // Amount of results to display poweredBy: true, // Controls displaying the logo (mandatory with our FREE plan) + placeholder: 'Search...'; // Input placeholder + openOnFocus: true; // Open search panel with default search, when focusing input + + // Theme + theme: { + mark: '#fff', // Color of the matching content + background: '#23263b', // Background Color of the input and the panel + selected: '#111432', // Background Color of the selected item + text: '#d6d6e7' // Color of the title of the items + } }); ``` @@ -70,24 +77,26 @@ algoliasearchNetlify({ }); ``` -## Scripts - -- `yarn dev`: run dev environment -- `yarn release`: build & publish the library +## Theme -## Development +You can theme the input and the autocomplete by using the `theme` property. -From this folder: - -```sh -yarn dev +```js +// Example of dark theme: +{ + theme: { + mark: '#fff', + background: '#23263b', + selected: '#111432', + text: '#d6d6e7' + } +} ``` -Or from the root of the repository: +Dark theme -```sh -yarn dev:frontend -``` +To go further you should take a look at the [autocomplete.js documentation](https://algolia-autocomplete.netlify.app/), or implement your own search with [InstantSearch.js](https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/js/). + +## Development & Release -This runs a `webpack-dev-server` on port 9100. -Meant to be used in conjunction with the [test website](../public/). +See [CONTRIBUTING.md](./CONTRIBUTING.md). diff --git a/frontend/package.json b/frontend/package.json index 1c0dc21c8..ee27e06c0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,11 +18,12 @@ "postinstall": "[ -d dist/ ] || npm run build" }, "devDependencies": { + "@algolia/autocomplete-js": "1.0.0-alpha.35", + "@algolia/autocomplete-preset-algolia": "1.0.0-alpha.29", "@algolia/transporter": "4.6.0", "@babel/core": "7.12.9", "@babel/preset-env": "7.12.7", "algoliasearch": "4.6.0", - "autocomplete.js": "0.38.0", "babel-loader": "8.2.2", "clean-webpack-plugin": "3.0.0", "core-js": "3.8.0", @@ -37,9 +38,9 @@ "sass-loader": "10.1.0", "terser-webpack-plugin": "4.2.3", "ts-loader": "8.0.11", - "webpack": "4.44.2", "webpack-cli": "4.2.0", - "webpack-dev-server": "3.11.0" + "webpack-dev-server": "3.11.0", + "webpack": "4.44.2" }, "keywords": [ "algolia", diff --git a/frontend/src/AlgoliasearchNetlify.ts b/frontend/src/AlgoliasearchNetlify.ts index dae9bd481..5cb6c3962 100644 --- a/frontend/src/AlgoliasearchNetlify.ts +++ b/frontend/src/AlgoliasearchNetlify.ts @@ -1,66 +1,51 @@ import { AutocompleteWrapper } from './AutocompleteWrapper'; -import { Options } from './options'; +import type { Options } from './types'; const defaultOptions: Omit< Options, - 'appId' | 'apiKey' | 'indexName' | 'siteId' | 'branch' + 'appId' | 'apiKey' | 'selector' | 'siteId' | 'branch' > = { analytics: true, - autocomplete: { - hitsPerPage: 5, - inputSelector: 'input[type=search]', - }, - color: '#3c4fe0', + hitsPerPage: 5, debug: false, - silenceWarnings: false, poweredBy: true, + placeholder: 'Search...', + openOnFocus: false, }; -class AlgoliasearchNetlify { - static instances: AlgoliasearchNetlify[]; - - search: AutocompleteWrapper; - - constructor(_options: Options) { - AlgoliasearchNetlify.instances.push(this); - - // Temporary - const splitIndexName = ( - indexName: string - ): { siteId: string; branch: string } => { - const regexp = /^netlify_([0-9a-f-]+)_(.*)_all$/; - const [, siteId, branch] = indexName.match(regexp)!; - return { siteId, branch }; - }; - - // eslint-disable-next-line no-warning-comments - // TODO: add validation - const options = { - ...defaultOptions, - ..._options, - ...(_options.indexName && splitIndexName(_options.indexName)), // Temporary - autocomplete: { - ...defaultOptions.autocomplete, - ..._options.autocomplete, - }, - }; - - this.search = new AutocompleteWrapper(options); - - // Wait for DOM initialization, then render - const render = this.render.bind(this, options); - if (['complete', 'interactive'].includes(document.readyState)) { - render(); - } else { - document.addEventListener('DOMContentLoaded', render); - } +const mandatory: Array = [ + 'appId', + 'apiKey', + 'selector', + 'siteId', + 'branch', +]; + +const instances: AutocompleteWrapper[] = []; + +function algoliasearchNetlify(_options: Options) { + const options = { + ...defaultOptions, + ..._options, + }; + for (const key of mandatory) { + if (options[key]) continue; + + throw new Error(`[algoliasearch-netlify] Missing mandatory key: ${key}`); } - render(options: Options) { - this.search.render(options); + const autocomplete = new AutocompleteWrapper(options); + instances.push(autocomplete); + + // Wait for DOM initialization, then render + const render = () => { + autocomplete.render(); + }; + if (['complete', 'interactive'].includes(document.readyState)) { + render(); + } else { + document.addEventListener('DOMContentLoaded', render); } } -AlgoliasearchNetlify.instances = []; - -export { AlgoliasearchNetlify }; +export { algoliasearchNetlify }; diff --git a/frontend/src/AutocompleteWrapper.ts b/frontend/src/AutocompleteWrapper.ts index 1cf9bc548..cb8eb9f6f 100644 --- a/frontend/src/AutocompleteWrapper.ts +++ b/frontend/src/AutocompleteWrapper.ts @@ -1,126 +1,63 @@ -// Small hack to remove verticalAlign on the input -// Makes IE11 fail though -import { isMsie } from 'autocomplete.js/src/common/utils'; -if (!isMsie()) { - const css = require('autocomplete.js/src/autocomplete/css.js'); - delete css.input.verticalAlign; - delete css.inputWithNoHint.verticalAlign; -} - -import type { SearchClient, SearchIndex } from 'algoliasearch/lite'; -import type { RequestOptions } from '@algolia/transporter'; -import type { Hit } from '@algolia/client-search'; +import type { SearchClient } from 'algoliasearch/lite'; import algoliasearch from 'algoliasearch/lite'; -import autocomplete from 'autocomplete.js'; +import type { Hit } from '@algolia/client-search'; +import { + autocomplete, + AutocompleteApi, + highlightHit, + snippetHit, + AutocompleteSource, +} from '@algolia/autocomplete-js'; +import { getAlgoliaHits } from '@algolia/autocomplete-preset-algolia'; -import type { Options } from './options'; +import type { Options, AlgoliaRecord } from './types'; import { templates } from './templates'; -import { addCss } from './addCss'; // @ts-ignore import { version } from '../package.json'; -import { Data } from './data'; - -export type SizeModifier = null | 'xs' | 'sm'; - -type AutocompleteJs = any; - -const XS_WIDTH = 400; -const SM_WIDTH = 600; class AutocompleteWrapper { - // All fields are private because they're just here for debugging - private client: SearchClient; - private index: SearchIndex; - - private $inputs: HTMLInputElement[] = []; - private autocompletes: AutocompleteJs[] = []; - - constructor({ appId, apiKey, siteId, branch }: Options) { - this.client = this.createClient(appId, apiKey); - const indexName = this.computeIndexName(siteId, branch); - this.index = this.client.initIndex(indexName); + private options; + private indexName; + private client; + private autocomplete: AutocompleteApi | undefined; + + constructor(options: Options) { + this.options = options; + this.client = this.createClient(); + this.indexName = this.computeIndexName(); } - render({ - analytics, - autocomplete: { hitsPerPage, inputSelector }, - color, - debug, - silenceWarnings, - poweredBy, - }: Options) { - addCss(templates.autocomplete.css(color)); - - const $inputs = this.getInputs(inputSelector); - - if ($inputs.length === 0 && !silenceWarnings) { - const inputSelectorText = JSON.stringify(inputSelector); - console.warn( - [ - `[Algolia] No input matched our default selector ${inputSelectorText}`, - 'The integration needs a search input to be active on your page. You can either:', - '- add an input that matches the selector', - '- or modify `autocomplete.inputSelector` to match your search input.', - ].join('\n') + render() { + const $input = document.querySelector(this.options.selector) as HTMLElement; + if (!$input) { + console.error( + `[algoliasearch-netlify] no element ${this.options.selector} found` ); + return; } - const autocompletes = $inputs.map(($input) => { - const inputWidth = $input.getBoundingClientRect().width; - - const sizeModifier = this.computeSizeModifier(inputWidth); - const nbSnippetWords = this.computeNbSnippetWords(inputWidth); - - const autocompleteParams = { - hint: false, - debug, - templates: this.getDropdownTemplates(poweredBy), - appendTo: 'body', - }; - - const searchParams = { - analytics, - hitsPerPage, - highlightPreTag: '', - highlightPostTag: '', - attributesToSnippet: [ - `description:${nbSnippetWords}`, - `content:${nbSnippetWords}`, - ], - snippetEllipsisText: '...', - }; - - const sources = [ - { - name: 'hits', - source: this.createSource(searchParams), - templates: { - suggestion: this.createRenderSuggestion(sizeModifier), - }, - }, - ]; - - const aa: AutocompleteJs = autocomplete( - $input, - autocompleteParams, - sources - ); - - const handleSelected = this.createHandleSelected(); - aa.on('autocomplete:selected', handleSelected); - - return aa; + const instance = autocomplete({ + container: $input, + autoFocus: false, + placeholder: this.options.placeholder, + debug: this.options.debug, + openOnFocus: this.options.openOnFocus, + panelPlacement: 'input-wrapper-width', + getSources: () => { + return [this.getSources()]; + }, }); + this.applyTheme($input.firstElementChild as HTMLElement); - // Store debug variables - this.$inputs = $inputs; - this.autocompletes = autocompletes; + this.autocomplete = instance; } - private computeIndexName(siteId: string, branch: string): string { + private computeIndexName(): string { + const { siteId, branch } = this.options; + // Keep in sync with crawler code in /netlify/crawl const cleanBranch = branch .trim() @@ -130,79 +67,82 @@ class AutocompleteWrapper { return `netlify_${siteId}_${cleanBranch}_all`; } - private createClient(appId: string, apiKey: string): SearchClient { - const client = algoliasearch(appId, apiKey); + private createClient(): SearchClient { + const client = algoliasearch(this.options.appId, this.options.apiKey); client.addAlgoliaAgent(`Netlify integration ${version}`); return client; } - private getInputs(inputSelector: string): HTMLInputElement[] { - return Array.from(document.querySelectorAll(inputSelector)); - } - - private computeSizeModifier(inputWidth: number): SizeModifier | null { - if (inputWidth < XS_WIDTH) return 'xs'; - if (inputWidth < SM_WIDTH) return 'sm'; - return null; - } - - private computeNbSnippetWords(inputWidth: number): number { - if (inputWidth < XS_WIDTH) return 0; - if (inputWidth < SM_WIDTH) return 3 + Math.floor(inputWidth / 35); - return Math.floor(inputWidth / 20); - } - - private getDropdownTemplates(poweredBy: boolean): { footer?: string } { - if (!poweredBy) return {}; - const { hostname } = window.location; - const algoliaLogoHtml = templates.algolia(hostname); + private getSources(): AutocompleteSource { + const poweredBy = this.options.poweredBy; return { - ...(poweredBy && { - footer: templates.autocomplete.poweredBy(algoliaLogoHtml), - }), - }; - } - - private createSource(searchParams: RequestOptions) { - return (query: string, callback: (hits: Array>) => void) => { - this.index - .search('', { ...searchParams, query }) - .then((content) => { - callback(content.hits); + getItems: ({ query }) => { + return getAlgoliaHits({ + searchClient: this.client, + queries: [ + { + indexName: this.indexName, + query, + params: { + analytics: this.options.analytics, + hitsPerPage: this.options.hitsPerPage, + }, + }, + ], }); + }, + getItemUrl({ item }) { + return item.url; + }, + templates: { + header() { + return; + }, + item({ item }: { item: Hit }) { + return templates.item( + item, + highlightHit({ hit: item, attribute: 'title' }), + getSuggestionSnippet(item) + ); + }, + footer() { + if (poweredBy) { + return templates.poweredBy(window.location.host); + } + }, + }, }; } - private createRenderSuggestion(sizeModifier: SizeModifier) { - return (hit: Hit): string => { - return templates.autocomplete.suggestion({ - ...hit, - sizeModifier, - snippet: this.getSuggestionSnippet(hit), - }); - }; - } + private applyTheme(el: HTMLElement | null) { + if (!el || !this.options.theme) { + return; + } - private getSuggestionSnippet(hit: Hit): string { - const description = hit._snippetResult?.description!; - const content = hit._snippetResult?.content!; - if (!description || !content) { - if (description) return description.value; - if (content) return content.value; - return ''; + const theme = this.options.theme; + if (theme.mark) { + el.style.setProperty('--color-mark', theme.mark); + } + if (theme.background) { + el.style.setProperty('--color-background', theme.background); + } + if (theme.text) { + el.style.setProperty('--color-text', theme.text); + } + if (theme.selected) { + el.style.setProperty('--color-selected', theme.selected); } - if (description.matchLevel === 'full') return description.value; - if (content.matchLevel === 'full') return content.value; - if (description.matchLevel === 'partial') return description.value; - if (content.matchLevel === 'partial') return content.value; - return description.value; } +} - private createHandleSelected() { - return (_event: any, suggestion: { url: string }) => { - window.location.href = suggestion.url; - }; +function getSuggestionSnippet(hit: Hit): string | null { + if (hit._snippetResult?.description) { + return snippetHit({ hit, attribute: 'description' }); + } + if (hit._snippetResult?.content) { + return snippetHit({ hit, attribute: 'content' }); } + return hit.description || hit.content; } export { AutocompleteWrapper }; diff --git a/frontend/src/addCss.ts b/frontend/src/addCss.ts deleted file mode 100644 index 31d80372d..000000000 --- a/frontend/src/addCss.ts +++ /dev/null @@ -1,15 +0,0 @@ -export function addCss(css: string, $mainStyle: HTMLElement | null = null) { - const $usedSibling = - $mainStyle ?? - document.querySelector( - 'link[rel=stylesheet][href*="algoliasearchNetlify"]' - ) ?? - document.getElementsByTagName('head')[0].lastChild!; - const $styleTag = document.createElement('style'); - $styleTag.setAttribute('type', 'text/css'); - $styleTag.appendChild(document.createTextNode(css)); - return $usedSibling.parentNode!.insertBefore( - $styleTag, - $usedSibling.nextSibling - ); -} diff --git a/frontend/src/data.ts b/frontend/src/data.ts deleted file mode 100644 index 4130f8897..000000000 --- a/frontend/src/data.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface Data { - url: string; - - title: string; - description: string; - - content: string; -} diff --git a/frontend/src/escapeHTML.ts b/frontend/src/escapeHTML.ts deleted file mode 100644 index ef37d3a8d..000000000 --- a/frontend/src/escapeHTML.ts +++ /dev/null @@ -1,9 +0,0 @@ -// See https://stackoverflow.com/questions/7381974/which-characters-need-to-be-escaped-in-html -export function escapeHTML(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} diff --git a/frontend/src/index.scss b/frontend/src/index.scss index a05ad91d5..4aa0d0519 100644 --- a/frontend/src/index.scss +++ b/frontend/src/index.scss @@ -1,127 +1,197 @@ -/* Search by */ -.aa-powered-by-link { - display: inline-block; - width: 64px; - height: 18px; - text-indent: 101%; - overflow: hidden; - white-space: nowrap; - background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0ODUgMTIwIj48ZyBmaWxsPSJub25lIj48cGF0aCBmaWxsPSIjNTQ2OEZGIiBkPSJNMTYuOC0xLjAwMWg4OC40YzguNyAwIDE1LjggNy4wNjUgMTUuOCAxNS44djg4LjQwNWMwIDguNy03LjA2NSAxNS43OTUtMTUuOCAxNS43OTVIMTYuOGMtOC43IDAtMTUuOC03LjA2LTE1LjgtMTUuNzk1VjE0Ljc1OWMwLTguNjk1IDcuMDYtMTUuNzYgMTUuOC0xNS43NiIvPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik03My41MDUgMjUuNzg4di00LjExNWE1LjIwOSA1LjIwOSAwIDAwLTUuMjEtNS4yMDVINTYuMTVhNS4yMDkgNS4yMDkgMCAwMC01LjIxIDUuMjA1djQuMjI1YzAgLjQ3LjQzNS44LjkxLjY5YTM3Ljk2NiAzNy45NjYgMCAwMTEwLjU3LTEuNDljMy40NjUgMCA2Ljg5NS40NyAxMC4yMSAxLjM4LjQ0LjExLjg3NS0uMjE1Ljg3NS0uNjlNNDAuMjIgMzEuMTczbC0yLjA3NS0yLjA3NWE1LjIwNiA1LjIwNiAwIDAwLTcuMzY1IDBsLTIuNDggMi40NzVhNS4xODUgNS4xODUgMCAwMDAgNy4zNTVsMi4wNCAyLjA0Yy4zMy4zMjUuODA1LjI1IDEuMDk1LS4wNzVhMzkuODc2IDM5Ljg3NiAwIDAxMy45NzUtNC42NiAzNy42OCAzNy42OCAwIDAxNC43LTRjLjM2NC0uMjIuNC0uNzMuMTEtMS4wNm0yMi4xNjQgMTMuMDY1djE3LjhjMCAuNTEuNTUuODc1IDEuMDIuNjJsMTUuODI1LTguMTljLjM2LS4xOC40Ny0uNjIuMjktLjk4LTMuMjgtNS43NTUtOS4zNy05LjY4NS0xNi40MDUtOS45NC0uMzY1IDAtLjczLjI5LS43My42OW0wIDQyLjg4Yy0xMy4xOTUgMC0yMy45MTUtMTAuNzA1LTIzLjkxNS0yMy44OCAwLTEzLjE3NSAxMC43Mi0yMy44NzUgMjMuOTE1LTIzLjg3NSAxMy4yIDAgMjMuOTE2IDEwLjcgMjMuOTE2IDIzLjg3NXMtMTAuNjggMjMuODgtMjMuOTE2IDIzLjg4bTAtNTcuOGMtMTguNzQgMC0zMy45NCAxNS4xOC0zMy45NCAzMy45MiAwIDE4Ljc0NSAxNS4yIDMzLjg5IDMzLjk0IDMzLjg5czMzLjk0LTE1LjE4IDMzLjk0LTMzLjkyNWMwLTE4Ljc0NS0xNS4xNjUtMzMuODg1LTMzLjk0LTMzLjg4NSIvPjxwYXRoIGZpbGw9IiM1NDY4RkYiIGQ9Ik0yNDAuMjE0IDk0LjE3N2MtMjMuMzY1LjExLTIzLjM2NS0xOC44NTUtMjMuMzY1LTIxLjg3NWwtLjA0LTcxLjA0NSAxNC4yNTQtMi4yNnY3MC42MWMwIDEuNzE1IDAgMTIuNTYgOS4xNSAxMi41OTV2MTEuOTc1aC4wMDF6bS01Ny43OC0xMS42MWM0LjM3NCAwIDcuNjItLjI1NSA5Ljg4LS42OVY2Ny4zOTJhMjkuMTk2IDI5LjE5NiAwIDAwLTMuNDMtLjY5NSAzMy43NDIgMzMuNzQyIDAgMDAtNC45NTYtLjM2NWMtMS41NyAwLTMuMTc1LjExLTQuNzc1LjM2NS0xLjYwNS4yMi0zLjA2NS42NTUtNC4zNCAxLjI3NS0xLjI3NS42Mi0yLjMzNSAxLjQ5NS0zLjEgMi42Mi0uOCAxLjEzLTEuMTY1IDEuNzg1LTEuMTY1IDMuNDk1IDAgMy4zNDUgMS4xNjUgNS4yOCAzLjI4IDYuNTUgMi4xMTUgMS4yNzUgNC45OTUgMS45MyA4LjYwNiAxLjkzem0tMS4yNC01MS42ODVjNC43IDAgOC42NzQuNTg1IDExLjg4NCAxLjc1IDMuMjA2IDEuMTY1IDUuNzk2IDIuOCA3LjY5IDQuODc1IDEuOTM1IDIuMTEgMy4yNDUgNC45MTUgNC4wNDYgNy45Ljg0IDIuOTg1IDEuMjQgNi4yNiAxLjI0IDkuODZ2MzYuNjJjLTIuMTg1LjQ3LTUuNTA2IDEuMDE1LTkuOTUgMS42Ny00LjQ0Ni42NTUtOS40NC45ODUtMTQuOTg2Ljk4NS0zLjY4IDAtNy4wNy0uMzY1LTEwLjA5NS0xLjA1NS0zLjA2NS0uNjktNS42NS0xLjgyLTcuODQtMy4zODUtMi4xNS0xLjU2NS0zLjgyNS0zLjU3LTUuMDY1LTYuMDQtMS4yMDUtMi40OC0xLjgyNS01Ljk3LTEuODI1LTkuNjEgMC0zLjQ5NS42OS01LjcxNSAyLjA0NS04LjEyIDEuMzgtMi40IDMuMjQtNC4zNjUgNS41NzUtNS44OTUgMi4zNy0xLjUzIDUuMDY1LTIuNjIgOC4xNjUtMy4yNzUgMy4xLS42NTUgNi4zNDUtLjk4NSA5LjY5NS0uOTg1IDEuNTcgMCAzLjIxLjExIDQuOTYuMjkgMS43MTUuMTg1IDMuNTc1LjUxNSA1LjU0NS45ODV2LTIuMzNjMC0xLjYzNS0uMTg1LTMuMi0uNTg1LTQuNjU1YTEwLjAxMiAxMC4wMTIgMCAwMC0yLjA0NS0zLjg5NWMtLjk4NS0xLjEzLTIuMjU1LTIuMDA1LTMuODYtMi42Mi0xLjYwNS0uNjItMy42NS0xLjA5NS02LjA5LTEuMDk1LTMuMjggMC02LjI3LjQtOS4wMDUuODc1LTIuNzM1LjQ3LTQuOTk1IDEuMDItNi43MSAxLjYzNWwtMS43MS0xMS42OGMxLjc4NS0uNjIgNC40NDUtMS4yNCA3Ljg3NS0xLjg1NSAzLjQyNS0uNjYgNy4xMS0uOTUgMTEuMDQ1LS45NWguMDAxem0yODEuNTEgNTEuMjg1YzQuMzc1IDAgNy42MTUtLjI1NSA5Ljg3NS0uNjk1di0xNC40OGMtLjgtLjIyLTEuOTMtLjQ3NS0zLjQyNS0uNjk1YTMzLjgxMyAzMy44MTMgMCAwMC00Ljk2LS4zNjVjLTEuNTY1IDAtMy4xNy4xMS00Ljc3NS4zNjUtMS42LjIyLTMuMDYuNjU1LTQuMzM1IDEuMjc1LTEuMjguNjItMi4zMzUgMS40OTUtMy4xIDIuNjItLjgwNSAxLjEzLTEuMTY1IDEuNzg1LTEuMTY1IDMuNDk1IDAgMy4zNDUgMS4xNjUgNS4yOCAzLjI4IDYuNTUgMi4xNSAxLjMxIDQuOTk1IDEuOTMgOC42MDUgMS45M3ptLTEuMjA1LTUxLjY0NWM0LjcgMCA4LjY3NC41OCAxMS44ODQgMS43NDUgMy4yMDUgMS4xNjUgNS43OTUgMi44IDcuNjkgNC44NzUgMS44OTUgMi4wNzUgMy4yNDUgNC45MTUgNC4wNDUgNy45Ljg0IDIuOTg1IDEuMjQgNi4yNiAxLjI0IDkuODY1djM2LjYxNWMtMi4xODUuNDctNS41MDUgMS4wMTUtOS45NSAxLjY3NS00LjQ0NS42NTUtOS40NC45OC0xNC45ODUuOTgtMy42OCAwLTcuMDctLjM2NS0xMC4wOTQtMS4wNTUtMy4wNjUtLjY5LTUuNjUtMS44Mi03Ljg0LTMuMzg1LTIuMTUtMS41NjUtMy44MjUtMy41Ny01LjA2NS02LjA0LTEuMjA1LTIuNDc1LTEuODI1LTUuOTctMS44MjUtOS42MSAwLTMuNDk1LjY5NS01LjcxNSAyLjA0NS04LjEyIDEuMzgtMi40IDMuMjQtNC4zNjUgNS41NzUtNS44OTUgMi4zNy0xLjUyNSA1LjA2NS0yLjYyIDguMTY1LTMuMjc1IDMuMS0uNjU1IDYuMzQ1LS45OCA5LjctLjk4IDEuNTY1IDAgMy4yMDUuMTEgNC45NTUuMjlzMy41NzUuNTEgNS41NC45ODV2LTIuMzNjMC0xLjY0LS4xOC0zLjIwNS0uNTgtNC42NmE5Ljk3NyA5Ljk3NyAwIDAwLTIuMDQ1LTMuODk1Yy0uOTg1LTEuMTMtMi4yNTUtMi4wMDUtMy44Ni0yLjYyLTEuNjA2LS42Mi0zLjY1LTEuMDktNi4wOS0xLjA5LTMuMjggMC02LjI3LjQtOS4wMDUuODctMi43MzUuNDc1LTQuOTk1IDEuMDItNi43MSAxLjY0bC0xLjcxLTExLjY4NWMxLjc4NS0uNjIgNC40NDUtMS4yMzUgNy44NzUtMS44NTUgMy40MjUtLjYyIDcuMTA1LS45NDUgMTEuMDQ1LS45NDV6bS00Mi44LTYuNzdjNC43NzQgMCA4LjY4LTMuODYgOC42OC04LjYzIDAtNC43NjUtMy44NjYtOC42MjUtOC42OC04LjYyNS00LjgxIDAtOC42NzUgMy44Ni04LjY3NSA4LjYyNSAwIDQuNzcgMy45IDguNjMgOC42NzUgOC42M3ptNy4xOCA3MC40MjVoLTE0LjMyNnYtNjEuNDRsMTQuMzI1LTIuMjU1djYzLjY5NWguMDAxem0tMjUuMTE2IDBjLTIzLjM2NS4xMS0yMy4zNjUtMTguODU1LTIzLjM2NS0yMS44NzVsLS4wNC03MS4wNDUgMTQuMjU1LTIuMjZ2NzAuNjFjMCAxLjcxNSAwIDEyLjU2IDkuMTUgMTIuNTk1djExLjk3NXptLTQ2LjMzNS0zMS40NDVjMC02LjE1NS0xLjM1LTExLjI4NS0zLjk3NC0xNC44NS0yLjYyNS0zLjYwNS02LjMwNS01LjM4NS0xMS4wMS01LjM4NS00LjcgMC04LjM4NiAxLjc4LTExLjAwNiA1LjM4NS0yLjYyNSAzLjYtMy45MDQgOC42OTUtMy45MDQgMTQuODUgMCA2LjIyNSAxLjMxNSAxMC40MDUgMy45NCAxNC4wMSAyLjYyNSAzLjY0IDYuMzA1IDUuNDI1IDExLjAxIDUuNDI1IDQuNyAwIDguMzg1LTEuODIgMTEuMDEtNS40MjUgMi42MjQtMy42NCAzLjkzNC03Ljc4NSAzLjkzNC0xNC4wMXptMTQuNTgtLjAzNWMwIDQuODA1LS42OSA4LjQ0LTIuMTE0IDEyLjQxLTEuNDIgMy45NjUtMy40MjUgNy4zNS02LjAxIDEwLjE1NS0yLjU5IDIuOC01LjY5IDQuOTg1LTkuMzM2IDYuNTE1LTMuNjQ0IDEuNTI1LTkuMjYgMi40LTEyLjA2NSAyLjQtMi44MS0uMDM1LTguMzg1LS44MzUtMTEuOTk1LTIuNC0zLjYxLTEuNTY1LTYuNzEtMy43MTUtOS4yOTUtNi41MTUtMi41OS0yLjgwNS00LjU5NC02LjE5LTYuMDU0LTEwLjE1NS0xLjQ1Ni0zLjk3LTIuMTg1LTcuNjA1LTIuMTg1LTEyLjQxcy42NTQtOS40MyAyLjExNC0xMy4zNmMxLjQ2LTMuOTMgMy41LTcuMjggNi4xMjUtMTAuMDggMi42MjUtMi44MDUgNS43Ni00Ljk1NSA5LjMzLTYuNDggMy42MS0xLjUzIDcuNTg1LTIuMjU1IDExLjg4NS0yLjI1NSA0LjMwNSAwIDguMjc1Ljc2IDExLjkyIDIuMjU1IDMuNjUgMS41MjUgNi43ODYgMy42NzUgOS4zMzYgNi40OCAyLjU4NCAyLjggNC41OSA2LjE1IDYuMDUgMTAuMDggMS41MyAzLjkzIDIuMjk1IDguNTU1IDIuMjk1IDEzLjM2aC0uMDAxem0tMTA3LjI4NCAwYzAgNS45NjUgMS4zMSAxMi41OSAzLjkzNSAxNS4zNTUgMi42MjUgMi43NyA2LjAxNCA0LjE1IDEwLjE3NSA0LjE1IDIuMjYgMCA0LjQxLS4zMjUgNi40MTQtLjk0NSAyLjAwNS0uNjIgMy42MDYtMS4zNSA0Ljg4Ni0yLjIydi0zNS4zNGMtMS4wMi0uMjItNS4yODYtMS4wOTUtOS40MS0xLjItNS4xNzUtLjE1LTkuMTEgMS45NjUtMTEuODggNS4zNDUtMi43MzYgMy4zOS00LjEyIDkuMzItNC4xMiAxNC44NTV6bTM5LjYyNSAyOC4wOTVjMCA5LjcyLTIuNDggMTYuODE1LTcuNDc2IDIxLjMzLTQuOTkgNC41MS0xMi42MSA2Ljc3LTIyLjg5IDYuNzctMy43NTUgMC0xMS41NTUtLjczLTE3Ljc5LTIuMTFsMi4yOTUtMTEuMjg1YzUuMjE1IDEuMDkgMTIuMTA1IDEuMzg1IDE1LjcxNSAxLjM4NSA1LjcyIDAgOS44MDUtMS4xNjUgMTIuMjQ1LTMuNDk1IDIuNDQ1LTIuMzMgMy42NDUtNS43ODUgMy42NDUtMTAuMzc1di0yLjMzYy0xLjQyLjY5LTMuMjggMS4zODUtNS41NzUgMi4xMTUtMi4yOTUuNjktNC45NTUgMS4wNTUtNy45NSAxLjA1NS0zLjkzNSAwLTcuNTEtLjYyLTEwLjc1LTEuODYtMy4yNDUtMS4yMzUtNi4wNTUtMy4wNTUtOC4zNS01LjQ2LTIuMjk1LTIuNC00LjEyLTUuNDItNS4zOTUtOS4wMjUtMS4yNzUtMy42MDUtMS45MzUtMTAuMDQ1LTEuOTM1LTE0Ljc3NSAwLTQuNDQuNjk1LTEwLjAxIDIuMDQ2LTEzLjcyNSAxLjM4NC0zLjcxIDMuMzUtNi45MTUgNi4wMTQtOS41NyAyLjYyNi0yLjY1NSA1LjgzNS00LjY5NSA5LjU5LTYuMTkgMy43NTUtMS40OSA4LjE2LTIuNDM1IDEyLjkzNS0yLjQzNSA0LjYzNSAwIDguOS41OCAxMy4wNTUgMS4yNzUgNC4xNTUuNjkgNy42OSAxLjQxNSAxMC41NyAyLjIxNXY1Ni40OWguMDAxeiIvPjwvZz48L3N2Zz4=); - background-repeat: no-repeat; - background-size: contain; - vertical-align: middle; +$color-bg: #fff; +$color-muted: #969faf; +$color-light: #797979; +$color-text: #23263b; +$color-mark: rgb(84, 104, 255); +$color-bg-selected: #f5f5fa; + +$font-size-xs: 12px; +$font-size-s: 14px; +$font-size-m: 16px; + +$size-s: 4px; +$size-m: 8px; +$size-l: 16px; +$size-l: 32px; + +.aa-Autocomplete { + --color-mark: #{$color-mark}; + --color-background: #{$color-bg}; + --color-selected: #{$color-bg-selected}; + --color-text: #{$color-text}; +} +// Source (modified) https://github.com/algolia/autocomplete.js/blob/next/examples/js/autocomplete.css +.aa-Form { + position: relative; +} + +.aa-Label { + align-items: center; + color: #777; + cursor: initial; + display: flex; + height: $size-l; + width: $size-l; + padding: 0 0.5rem; + position: absolute; + z-index: 2; +} + +.aa-InputWrapper { + background-color: $color-bg; + background-color: var(--color-background); + max-width: 100%; + position: relative; + width: 100%; + display: flex; + border-radius: 3px; } -.clearfix { - clear: both; +.aa-Input { + appearance: none; + background: none; + border: 1px solid #d6d6e7; + border-radius: 3px; + box-shadow: rgba(119, 122, 175, 0.3) 0 1px 4px 0 inset; + caret-color: #5a5e9a; + color: $color-text; + color: var(--color-text); + height: $size-l; + width: 100%; + z-index: 2; + font-size: $font-size-m; } -/***************************/ -/* autocomplete.js */ -/***************************/ +.aa-Input::-webkit-search-decoration, +.aa-Input::-webkit-search-cancel-button, +.aa-Input::-webkit-search-results-button, +.aa-Input::-webkit-search-results-decoration { + -webkit-appearance: none; +} -.algolia-autocomplete { - width: 100%; - line-height: normal; +.aa-Input { + padding: 0 2.25rem; } -.aa-input { - width: 100%; - outline: none; +.aa-Input::placeholder { + color: #5a5e9a; } -.aa-hint { - width: 100%; - color: #999; +.aa-Input:focus { + border-color: #3c4fe0; + box-shadow: rgba(35, 38, 59, 0.05) 0 1px 0 0; + outline: currentcolor none medium; } -.aa-dropdown-menu { - margin-top: 9px; - border: 1px solid #d9d9d9; +.aa-ResetButton { + background: none; + border: 0; + cursor: pointer; + color: #777; + height: $size-l; + width: $size-l; + position: absolute; + right: 0; + z-index: 2; + display: inline-flex; + justify-content: center; + align-items: center; +} + +.aa-Panel { + background-color: $color-bg; + background-color: var(--color-background); + border: 1px solid rgba(150, 150, 150, 0.16); border-radius: 3px; - background-color: #fff; - box-shadow: 0 1px 0 0 #ccc, 0 2px 3px 0 #e6e6e6; - font-size: 14px; - text-align: left; + box-shadow: 0 0 0 1px rgba(35, 38, 59, 0.05), + 0 8px 16px -4px rgba(35, 38, 59, 0.25); + margin-top: 5px; + max-width: 480px; + position: absolute; + width: 100%; + min-width: 400px; + font-size: $font-size-m; + font-weight: normal; + z-index: 100; + font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,Helvetica,Arial,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol; } -.aa-suggestion { - cursor: pointer; - padding: 8px 16px; +.aa-Panel--stalled { + filter: grayscale(1); + opacity: 0.5; + transition: opacity 200ms ease-in; +} - & ~ .aa-suggestion { - border-top: 1px solid #e8e8e8; - } +.aa-Panel a { + color: inherit; + text-decoration: none; } -.aa-suggestion.aa-cursor { - background-color: #f8f8f8; +.aa-Panel ul { + list-style: none; + margin: 0; + padding: 0; } -.aa-powered-by { - text-align: right; - font-size: 0.8em; - color: #999; - padding: 8px 16px 8px 0; -} - -// title -.aa-hit--title { - font-size: 1.1em; - font-weight: bold; - color: black; - - .aa-hit--highlight { - position: relative; - z-index: 1; - - &::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0.1; - z-index: -1; - } - } -} - -.aa-hit__xs .aa-hit--title, -.aa-hit__sm .aa-hit--title { - font-size: 1em; - line-height: 1.25em; - display: block; -} - -.aa-hit__xs .aa-hit--title { - font-weight: normal; - padding: 8px 0; +.aa-Item { + color: $color-text; + color: var(--color-text); + cursor: pointer; + display: flex; } -.aa-suggestion:first-child .aa-hit--title { - border: none; +.aa-Item[aria-selected='true'] { + background-color: $color-bg-selected; + background-color: var(--color-selected); } -/** body **/ -.aa-hit--description { - font-size: 13px; - color: #797979; - margin-top: 4px; - line-height: 1.25em; +.aa-ItemContent { + display: flex; + flex-grow: 1; + grid-gap: 0.5rem; + padding: 0.5rem; + text-align: left; +} - .aa-hit--highlight { - color: #666; - font-weight: bold; - } +.aa-ItemSourceIcon { + color: rgba(80, 80, 80, 0.32); } -.aa-hit__xs .aa-hit--description { - display: none; +// ---- More +.aa-ItemTitle { + font-size: $font-size-s; + line-height: 18px; +} +.aa-ItemDescription { + font-size: $font-size-xs; + line-height: 16px; + color: $color-light; } +.aa-ItemContent mark { + color: $color-mark; + color: var(--color-mark); + background-color: transparent; +} + + -.aa-hit__sm .aa-hit--description { - font-size: 0.9em; - line-height: 1.25em; +/* Search by */ +.aa-powered-by-link { + display: inline-block; + width: 64px; + height: 18px; + text-indent: 101%; + overflow: hidden; + white-space: nowrap; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0ODUgMTIwIj48ZyBmaWxsPSJub25lIj48cGF0aCBmaWxsPSIjNTQ2OEZGIiBkPSJNMTYuOC0xLjAwMWg4OC40YzguNyAwIDE1LjggNy4wNjUgMTUuOCAxNS44djg4LjQwNWMwIDguNy03LjA2NSAxNS43OTUtMTUuOCAxNS43OTVIMTYuOGMtOC43IDAtMTUuOC03LjA2LTE1LjgtMTUuNzk1VjE0Ljc1OWMwLTguNjk1IDcuMDYtMTUuNzYgMTUuOC0xNS43NiIvPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik03My41MDUgMjUuNzg4di00LjExNWE1LjIwOSA1LjIwOSAwIDAwLTUuMjEtNS4yMDVINTYuMTVhNS4yMDkgNS4yMDkgMCAwMC01LjIxIDUuMjA1djQuMjI1YzAgLjQ3LjQzNS44LjkxLjY5YTM3Ljk2NiAzNy45NjYgMCAwMTEwLjU3LTEuNDljMy40NjUgMCA2Ljg5NS40NyAxMC4yMSAxLjM4LjQ0LjExLjg3NS0uMjE1Ljg3NS0uNjlNNDAuMjIgMzEuMTczbC0yLjA3NS0yLjA3NWE1LjIwNiA1LjIwNiAwIDAwLTcuMzY1IDBsLTIuNDggMi40NzVhNS4xODUgNS4xODUgMCAwMDAgNy4zNTVsMi4wNCAyLjA0Yy4zMy4zMjUuODA1LjI1IDEuMDk1LS4wNzVhMzkuODc2IDM5Ljg3NiAwIDAxMy45NzUtNC42NiAzNy42OCAzNy42OCAwIDAxNC43LTRjLjM2NC0uMjIuNC0uNzMuMTEtMS4wNm0yMi4xNjQgMTMuMDY1djE3LjhjMCAuNTEuNTUuODc1IDEuMDIuNjJsMTUuODI1LTguMTljLjM2LS4xOC40Ny0uNjIuMjktLjk4LTMuMjgtNS43NTUtOS4zNy05LjY4NS0xNi40MDUtOS45NC0uMzY1IDAtLjczLjI5LS43My42OW0wIDQyLjg4Yy0xMy4xOTUgMC0yMy45MTUtMTAuNzA1LTIzLjkxNS0yMy44OCAwLTEzLjE3NSAxMC43Mi0yMy44NzUgMjMuOTE1LTIzLjg3NSAxMy4yIDAgMjMuOTE2IDEwLjcgMjMuOTE2IDIzLjg3NXMtMTAuNjggMjMuODgtMjMuOTE2IDIzLjg4bTAtNTcuOGMtMTguNzQgMC0zMy45NCAxNS4xOC0zMy45NCAzMy45MiAwIDE4Ljc0NSAxNS4yIDMzLjg5IDMzLjk0IDMzLjg5czMzLjk0LTE1LjE4IDMzLjk0LTMzLjkyNWMwLTE4Ljc0NS0xNS4xNjUtMzMuODg1LTMzLjk0LTMzLjg4NSIvPjxwYXRoIGZpbGw9IiM1NDY4RkYiIGQ9Ik0yNDAuMjE0IDk0LjE3N2MtMjMuMzY1LjExLTIzLjM2NS0xOC44NTUtMjMuMzY1LTIxLjg3NWwtLjA0LTcxLjA0NSAxNC4yNTQtMi4yNnY3MC42MWMwIDEuNzE1IDAgMTIuNTYgOS4xNSAxMi41OTV2MTEuOTc1aC4wMDF6bS01Ny43OC0xMS42MWM0LjM3NCAwIDcuNjItLjI1NSA5Ljg4LS42OVY2Ny4zOTJhMjkuMTk2IDI5LjE5NiAwIDAwLTMuNDMtLjY5NSAzMy43NDIgMzMuNzQyIDAgMDAtNC45NTYtLjM2NWMtMS41NyAwLTMuMTc1LjExLTQuNzc1LjM2NS0xLjYwNS4yMi0zLjA2NS42NTUtNC4zNCAxLjI3NS0xLjI3NS42Mi0yLjMzNSAxLjQ5NS0zLjEgMi42Mi0uOCAxLjEzLTEuMTY1IDEuNzg1LTEuMTY1IDMuNDk1IDAgMy4zNDUgMS4xNjUgNS4yOCAzLjI4IDYuNTUgMi4xMTUgMS4yNzUgNC45OTUgMS45MyA4LjYwNiAxLjkzem0tMS4yNC01MS42ODVjNC43IDAgOC42NzQuNTg1IDExLjg4NCAxLjc1IDMuMjA2IDEuMTY1IDUuNzk2IDIuOCA3LjY5IDQuODc1IDEuOTM1IDIuMTEgMy4yNDUgNC45MTUgNC4wNDYgNy45Ljg0IDIuOTg1IDEuMjQgNi4yNiAxLjI0IDkuODZ2MzYuNjJjLTIuMTg1LjQ3LTUuNTA2IDEuMDE1LTkuOTUgMS42Ny00LjQ0Ni42NTUtOS40NC45ODUtMTQuOTg2Ljk4NS0zLjY4IDAtNy4wNy0uMzY1LTEwLjA5NS0xLjA1NS0zLjA2NS0uNjktNS42NS0xLjgyLTcuODQtMy4zODUtMi4xNS0xLjU2NS0zLjgyNS0zLjU3LTUuMDY1LTYuMDQtMS4yMDUtMi40OC0xLjgyNS01Ljk3LTEuODI1LTkuNjEgMC0zLjQ5NS42OS01LjcxNSAyLjA0NS04LjEyIDEuMzgtMi40IDMuMjQtNC4zNjUgNS41NzUtNS44OTUgMi4zNy0xLjUzIDUuMDY1LTIuNjIgOC4xNjUtMy4yNzUgMy4xLS42NTUgNi4zNDUtLjk4NSA5LjY5NS0uOTg1IDEuNTcgMCAzLjIxLjExIDQuOTYuMjkgMS43MTUuMTg1IDMuNTc1LjUxNSA1LjU0NS45ODV2LTIuMzNjMC0xLjYzNS0uMTg1LTMuMi0uNTg1LTQuNjU1YTEwLjAxMiAxMC4wMTIgMCAwMC0yLjA0NS0zLjg5NWMtLjk4NS0xLjEzLTIuMjU1LTIuMDA1LTMuODYtMi42Mi0xLjYwNS0uNjItMy42NS0xLjA5NS02LjA5LTEuMDk1LTMuMjggMC02LjI3LjQtOS4wMDUuODc1LTIuNzM1LjQ3LTQuOTk1IDEuMDItNi43MSAxLjYzNWwtMS43MS0xMS42OGMxLjc4NS0uNjIgNC40NDUtMS4yNCA3Ljg3NS0xLjg1NSAzLjQyNS0uNjYgNy4xMS0uOTUgMTEuMDQ1LS45NWguMDAxem0yODEuNTEgNTEuMjg1YzQuMzc1IDAgNy42MTUtLjI1NSA5Ljg3NS0uNjk1di0xNC40OGMtLjgtLjIyLTEuOTMtLjQ3NS0zLjQyNS0uNjk1YTMzLjgxMyAzMy44MTMgMCAwMC00Ljk2LS4zNjVjLTEuNTY1IDAtMy4xNy4xMS00Ljc3NS4zNjUtMS42LjIyLTMuMDYuNjU1LTQuMzM1IDEuMjc1LTEuMjguNjItMi4zMzUgMS40OTUtMy4xIDIuNjItLjgwNSAxLjEzLTEuMTY1IDEuNzg1LTEuMTY1IDMuNDk1IDAgMy4zNDUgMS4xNjUgNS4yOCAzLjI4IDYuNTUgMi4xNSAxLjMxIDQuOTk1IDEuOTMgOC42MDUgMS45M3ptLTEuMjA1LTUxLjY0NWM0LjcgMCA4LjY3NC41OCAxMS44ODQgMS43NDUgMy4yMDUgMS4xNjUgNS43OTUgMi44IDcuNjkgNC44NzUgMS44OTUgMi4wNzUgMy4yNDUgNC45MTUgNC4wNDUgNy45Ljg0IDIuOTg1IDEuMjQgNi4yNiAxLjI0IDkuODY1djM2LjYxNWMtMi4xODUuNDctNS41MDUgMS4wMTUtOS45NSAxLjY3NS00LjQ0NS42NTUtOS40NC45OC0xNC45ODUuOTgtMy42OCAwLTcuMDctLjM2NS0xMC4wOTQtMS4wNTUtMy4wNjUtLjY5LTUuNjUtMS44Mi03Ljg0LTMuMzg1LTIuMTUtMS41NjUtMy44MjUtMy41Ny01LjA2NS02LjA0LTEuMjA1LTIuNDc1LTEuODI1LTUuOTctMS44MjUtOS42MSAwLTMuNDk1LjY5NS01LjcxNSAyLjA0NS04LjEyIDEuMzgtMi40IDMuMjQtNC4zNjUgNS41NzUtNS44OTUgMi4zNy0xLjUyNSA1LjA2NS0yLjYyIDguMTY1LTMuMjc1IDMuMS0uNjU1IDYuMzQ1LS45OCA5LjctLjk4IDEuNTY1IDAgMy4yMDUuMTEgNC45NTUuMjlzMy41NzUuNTEgNS41NC45ODV2LTIuMzNjMC0xLjY0LS4xOC0zLjIwNS0uNTgtNC42NmE5Ljk3NyA5Ljk3NyAwIDAwLTIuMDQ1LTMuODk1Yy0uOTg1LTEuMTMtMi4yNTUtMi4wMDUtMy44Ni0yLjYyLTEuNjA2LS42Mi0zLjY1LTEuMDktNi4wOS0xLjA5LTMuMjggMC02LjI3LjQtOS4wMDUuODctMi43MzUuNDc1LTQuOTk1IDEuMDItNi43MSAxLjY0bC0xLjcxLTExLjY4NWMxLjc4NS0uNjIgNC40NDUtMS4yMzUgNy44NzUtMS44NTUgMy40MjUtLjYyIDcuMTA1LS45NDUgMTEuMDQ1LS45NDV6bS00Mi44LTYuNzdjNC43NzQgMCA4LjY4LTMuODYgOC42OC04LjYzIDAtNC43NjUtMy44NjYtOC42MjUtOC42OC04LjYyNS00LjgxIDAtOC42NzUgMy44Ni04LjY3NSA4LjYyNSAwIDQuNzcgMy45IDguNjMgOC42NzUgOC42M3ptNy4xOCA3MC40MjVoLTE0LjMyNnYtNjEuNDRsMTQuMzI1LTIuMjU1djYzLjY5NWguMDAxem0tMjUuMTE2IDBjLTIzLjM2NS4xMS0yMy4zNjUtMTguODU1LTIzLjM2NS0yMS44NzVsLS4wNC03MS4wNDUgMTQuMjU1LTIuMjZ2NzAuNjFjMCAxLjcxNSAwIDEyLjU2IDkuMTUgMTIuNTk1djExLjk3NXptLTQ2LjMzNS0zMS40NDVjMC02LjE1NS0xLjM1LTExLjI4NS0zLjk3NC0xNC44NS0yLjYyNS0zLjYwNS02LjMwNS01LjM4NS0xMS4wMS01LjM4NS00LjcgMC04LjM4NiAxLjc4LTExLjAwNiA1LjM4NS0yLjYyNSAzLjYtMy45MDQgOC42OTUtMy45MDQgMTQuODUgMCA2LjIyNSAxLjMxNSAxMC40MDUgMy45NCAxNC4wMSAyLjYyNSAzLjY0IDYuMzA1IDUuNDI1IDExLjAxIDUuNDI1IDQuNyAwIDguMzg1LTEuODIgMTEuMDEtNS40MjUgMi42MjQtMy42NCAzLjkzNC03Ljc4NSAzLjkzNC0xNC4wMXptMTQuNTgtLjAzNWMwIDQuODA1LS42OSA4LjQ0LTIuMTE0IDEyLjQxLTEuNDIgMy45NjUtMy40MjUgNy4zNS02LjAxIDEwLjE1NS0yLjU5IDIuOC01LjY5IDQuOTg1LTkuMzM2IDYuNTE1LTMuNjQ0IDEuNTI1LTkuMjYgMi40LTEyLjA2NSAyLjQtMi44MS0uMDM1LTguMzg1LS44MzUtMTEuOTk1LTIuNC0zLjYxLTEuNTY1LTYuNzEtMy43MTUtOS4yOTUtNi41MTUtMi41OS0yLjgwNS00LjU5NC02LjE5LTYuMDU0LTEwLjE1NS0xLjQ1Ni0zLjk3LTIuMTg1LTcuNjA1LTIuMTg1LTEyLjQxcy42NTQtOS40MyAyLjExNC0xMy4zNmMxLjQ2LTMuOTMgMy41LTcuMjggNi4xMjUtMTAuMDggMi42MjUtMi44MDUgNS43Ni00Ljk1NSA5LjMzLTYuNDggMy42MS0xLjUzIDcuNTg1LTIuMjU1IDExLjg4NS0yLjI1NSA0LjMwNSAwIDguMjc1Ljc2IDExLjkyIDIuMjU1IDMuNjUgMS41MjUgNi43ODYgMy42NzUgOS4zMzYgNi40OCAyLjU4NCAyLjggNC41OSA2LjE1IDYuMDUgMTAuMDggMS41MyAzLjkzIDIuMjk1IDguNTU1IDIuMjk1IDEzLjM2aC0uMDAxem0tMTA3LjI4NCAwYzAgNS45NjUgMS4zMSAxMi41OSAzLjkzNSAxNS4zNTUgMi42MjUgMi43NyA2LjAxNCA0LjE1IDEwLjE3NSA0LjE1IDIuMjYgMCA0LjQxLS4zMjUgNi40MTQtLjk0NSAyLjAwNS0uNjIgMy42MDYtMS4zNSA0Ljg4Ni0yLjIydi0zNS4zNGMtMS4wMi0uMjItNS4yODYtMS4wOTUtOS40MS0xLjItNS4xNzUtLjE1LTkuMTEgMS45NjUtMTEuODggNS4zNDUtMi43MzYgMy4zOS00LjEyIDkuMzItNC4xMiAxNC44NTV6bTM5LjYyNSAyOC4wOTVjMCA5LjcyLTIuNDggMTYuODE1LTcuNDc2IDIxLjMzLTQuOTkgNC41MS0xMi42MSA2Ljc3LTIyLjg5IDYuNzctMy43NTUgMC0xMS41NTUtLjczLTE3Ljc5LTIuMTFsMi4yOTUtMTEuMjg1YzUuMjE1IDEuMDkgMTIuMTA1IDEuMzg1IDE1LjcxNSAxLjM4NSA1LjcyIDAgOS44MDUtMS4xNjUgMTIuMjQ1LTMuNDk1IDIuNDQ1LTIuMzMgMy42NDUtNS43ODUgMy42NDUtMTAuMzc1di0yLjMzYy0xLjQyLjY5LTMuMjggMS4zODUtNS41NzUgMi4xMTUtMi4yOTUuNjktNC45NTUgMS4wNTUtNy45NSAxLjA1NS0zLjkzNSAwLTcuNTEtLjYyLTEwLjc1LTEuODYtMy4yNDUtMS4yMzUtNi4wNTUtMy4wNTUtOC4zNS01LjQ2LTIuMjk1LTIuNC00LjEyLTUuNDItNS4zOTUtOS4wMjUtMS4yNzUtMy42MDUtMS45MzUtMTAuMDQ1LTEuOTM1LTE0Ljc3NSAwLTQuNDQuNjk1LTEwLjAxIDIuMDQ2LTEzLjcyNSAxLjM4NC0zLjcxIDMuMzUtNi45MTUgNi4wMTQtOS41NyAyLjYyNi0yLjY1NSA1LjgzNS00LjY5NSA5LjU5LTYuMTkgMy43NTUtMS40OSA4LjE2LTIuNDM1IDEyLjkzNS0yLjQzNSA0LjYzNSAwIDguOS41OCAxMy4wNTUgMS4yNzUgNC4xNTUuNjkgNy42OSAxLjQxNSAxMC41NyAyLjIxNXY1Ni40OWguMDAxeiIvPjwvZz48L3N2Zz4=); + background-repeat: no-repeat; + background-size: contain; + vertical-align: middle; +} +.aa-powered-by { + text-align: right; + font-size: $font-size-xs; + color: $color-muted; + padding: $size-m $size-m $size-s 0; + font-weight: normal; } diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 974d9c8a0..798936f4e 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -1,5 +1,4 @@ -import { AlgoliasearchNetlify } from './AlgoliasearchNetlify'; -import { toFactory } from './toFactory'; +import { algoliasearchNetlify } from './AlgoliasearchNetlify'; // eslint-disable-next-line import/no-commonjs -module.exports = toFactory(AlgoliasearchNetlify); +module.exports = algoliasearchNetlify; diff --git a/frontend/src/options.ts b/frontend/src/options.ts deleted file mode 100644 index 51f790ce6..000000000 --- a/frontend/src/options.ts +++ /dev/null @@ -1,21 +0,0 @@ -export interface Options { - // Mandatory - appId: string; - apiKey: string; - - // Temporary - indexName?: string; - siteId: string; - branch: string; - - // Optional - analytics: boolean; - autocomplete: { - hitsPerPage: number; - inputSelector: string; - }; - color: string; - debug: boolean; - silenceWarnings: boolean; - poweredBy: boolean; -} diff --git a/frontend/src/templates.ts b/frontend/src/templates.ts index 01dc78e33..974121b8b 100644 --- a/frontend/src/templates.ts +++ b/frontend/src/templates.ts @@ -1,51 +1,41 @@ -import type { Hit } from '@algolia/client-search'; +import { AlgoliaRecord } from './types'; -import { SizeModifier } from './AutocompleteWrapper'; -import { Data } from './data'; -import { escapeHTML } from './escapeHTML'; - -export interface Templates { - algolia: (hostname: string) => string; - autocomplete: { - css: (color: string) => string; - poweredBy: (algoliaLogoHtml: string) => string; - suggestion: ( - hit: Hit & { sizeModifier: SizeModifier; snippet: string } - ) => string; - }; -} - -export const templates: Templates = { - algolia: (hostname) => { - const escapedHostname = escapeHTML(hostname); +export const templates = { + poweredBy: (hostname: string) => { + const escapedHostname = encodeURIComponent(hostname); return ` - - Algolia - +
+ Search by + + Algolia + +
`; }, - autocomplete: { - css: (color) => ` - .aa-hit--highlight { - color: ${color}; - } - .aa-hit--title .aa-hit--highlight::before { - background-color: ${color}; - } - `, - poweredBy: (algoliaLogoHtml) => ` -
- Search by ${algoliaLogoHtml} -
`, - suggestion: (hit) => ` -
-
${hit._highlightResult!.title.value}
-
${hit.snippet}
-
- `, + item: (record: AlgoliaRecord, title: string, description: string | null) => { + return ` + +
+
+ +
+
+
+ ${title} +
+ ${ + description + ? `
${description}
` + : '' + } +
+
+
+ + `; }, }; diff --git a/frontend/src/toFactory.ts b/frontend/src/toFactory.ts deleted file mode 100644 index f11ff82e2..000000000 --- a/frontend/src/toFactory.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Taken from https://github.com/timoxley/to-factory/ -// License: MIT - -export function toFactory(Class: any) { - // eslint-disable-next-line algolia/func-style-toplevel - const Factory = function (...args: any[]) { - return new Class(...args); - }; - // eslint-disable-next-line no-proto - Factory.__proto__ = Class; - Factory.prototype = Class.prototype; - return Factory; -} diff --git a/frontend/src/global.d.ts b/frontend/src/types/global.d.ts similarity index 100% rename from frontend/src/global.d.ts rename to frontend/src/types/global.d.ts diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 000000000..267272ce7 --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,2 @@ +export * from './options'; +export * from './record'; diff --git a/frontend/src/types/options.ts b/frontend/src/types/options.ts new file mode 100644 index 000000000..2e5bda0ee --- /dev/null +++ b/frontend/src/types/options.ts @@ -0,0 +1,22 @@ +export interface Options { + // Mandatory + appId: string; + apiKey: string; + selector: string; + siteId: string; + branch: string; + + // Optional + analytics?: boolean; + hitsPerPage?: number; + theme?: { + mark?: string; + background?: string; + selected?: string; + text?: string; + }; + debug?: boolean; + placeholder?: string; + openOnFocus?: boolean; + poweredBy?: boolean; +} diff --git a/frontend/src/types/record.ts b/frontend/src/types/record.ts new file mode 100644 index 000000000..f34aa088f --- /dev/null +++ b/frontend/src/types/record.ts @@ -0,0 +1,20 @@ +export type AlgoliaRecord = { + objectID: string; + + url: string; + origin: string; + title: string; + content: string; + + lang?: string; + description?: string; + keywords?: string[]; + image?: string; + authors?: string[]; + datePublished?: number; + dateModified?: number; + category?: string; + + urlDepth?: number; + position?: number; +}; diff --git a/public/index.html b/public/index.html index a408127cd..1d777a867 100644 --- a/public/index.html +++ b/public/index.html @@ -33,13 +33,18 @@ } #search { + display: flex; + gap: 16px; + } + + #searchBig { width: 60%; - display: block; - margin: 16px auto; - padding: 8px 16px; - font-size: 1.2em; - border-radius: 4px; - border: 1px solid #ccc; + height: 32px; + } + + #searchSmall { + height: 32px; + width: 20%; } #debug-script { @@ -103,7 +108,10 @@

Algoliasearch Netlify Test Website

- +

Script

@@ -146,12 +154,29 @@

Test content