feat: promote DocSearch v5 to main (#2968)
* feat(askai): add compatibility with algolia mcp search tool [DASH-2294] (#2862) ## Summary Fixes DASH-2294 Add compatibility with the Algolia MCP search tool (`algolia_search_index_${string}`) in AskAI. ## Changes - Add `AlgoliaMCPSearchTool` type to handle the Algolia MCP server search tool - Refactor how number of hits are retrieved in `ToolCall` into a `getNumberOfHits` helper ## Test plan - Added unit tests for modified code ✅ * chore: Update to use tsdown for build system (#2824) * chore: Update to use tsdown for build system * fix: docsearch-react build * fix: lint * fix: glob resolved to incorrect version * chore: migrate to from yarn, lerna and shipjs to bun & changesets (#2827) * chore: add tool-versions file for node and bun versions (#2866) * chore: watch in parallel (#2867) * feat: agent studio feedback integration (#2868) * feat(askai): Split Ask AI modal into own component (#2884) * feat(askai): Split Ask AI modal into own component * refactor(react): share modal utilities * refactor(react): share search box form * refactor(react): extract start screen sections * refactor(react): extract shared modal hooks * fix: lint adapter * refactor(react): reorganize modal files * fix: type error in examples * fix: remove ai modal from adapter for now, fix import paths of react package * feat(askai): Agent Studio core tools (#2886) * feat(askai): Implement dynamic tool calls * move ToolCall to components dir * Converge Agent Studio search tools to same definition, fix client side tools breaking UI state * add examples for custom tools * fix: lint & types * feat(askai): add Agent Studio memory support (#2888) * feat(askai): remove Ask AI transport layer (#2889) * feat(askai): add Agent Studio memory support * refactor(askai): remove Ask AI transport abstraction * feat(askai): Feedback notes and tags (#2890) * feat(askai): add Agent Studio memory support * refactor(askai): remove Ask AI transport abstraction * feat(askai): Feedback notes and tags * fix: bump css bundle size limit * move feedback actions to components * chore: fix deploys for v5 branch * feat(askai): Aggregate MCP search tool calls (#2891) * feat(askai): add Agent Studio memory support * refactor(askai): remove Ask AI transport abstraction * feat(askai): Feedback notes and tags * fix: bump css bundle size limit * move feedback actions to components * feat(askai): Aggregate MCP search tool calls * feat(askai): Allow dynamic indices for Agent Studio (#2893) * feat(v5): UI updates (#2896) * feat(v5): UI updates * fix: css file size * fix: e2e tests * fix: e2e tests * fix: e2e tests * chore: add theme toggle to react demo example * Update sources panel display, update dark theme * fix: lint * fix(askai): address ui review feedback * fix: pin icon positioning * fix(askai): improve a11y and dark-mode shimmer for thinking and error states - add role=alert/status and aria-hidden on error/thinking UI - support dark-mode shimmer gradients via CSS variables - respect prefers-reduced-motion for shimmer - handle null date in useRelativeFormattedDate with fallback translation * feat(v5): Add hit breadcrumbs (#2897) * feat(v5): UI updates * fix: css file size * fix: e2e tests * fix: e2e tests * fix: e2e tests * chore: add theme toggle to react demo example * Update sources panel display, update dark theme * fix: lint * fix(askai): address ui review feedback * fix: pin icon positioning * fix(askai): improve a11y and dark-mode shimmer for thinking and error states - add role=alert/status and aria-hidden on error/thinking UI - support dark-mode shimmer gradients via CSS variables - respect prefers-reduced-motion for shimmer - handle null date in useRelativeFormattedDate with fallback translation * feat(v5): Add hit breadcrumbs * fix: bump css bundle size limit * Fix after conflicts * chore: move CSS building to lightning css (#2898) * feat: Facet filters for search (#2899) * feat(v5): Initial facet filters work * Perf updates, dark theme, facet chips, a11y improvements * fix: bump css bundle size limit * Dedupe facet filters, refetch facets on searchParameters changes * Add chevron flourish * fix(askai): Fix new conversation causing thread depth errors (#2900) * feat(v5): Add hit result badge (#2901) * feat(v5): Add hit result badge * Add background to hit result badge * feat(v5): Add follow up prompt suggestions (#2902) * feat(v5): Add follow up prompt suggestions * fix: bump css bundle size limit * docs(agents): document Cursor Cloud dev environment setup for v5 (Bun) (#2903) Co-authored-by: Cursor Agent <cursoragent@cursor.com> * feat(mcp): setup mcp plugins (#2895) * feat(v5): Add prompt suggestions to keyword search (#2912) * feat(v5): Add prompt suggestions to keyword search * cleanup: Move consistent object to reusable constant * chore(v5): Split Ask AI related CSS into own bundle (#2913) * chore(v5): Split Ask AI related CSS into own bundle * move style.css to include modal and askai * fix: Ensure stage level and watch level scripts use bun runtime (#2915) * fix: Ensure stage level and watch level scripts use bun runtime * chore: move to node@24 update imports * fix: lint * feat(js): Document JS based hybrid mode, fix JS packages (#2916) * feat(js): Document JS based hybrid mode, fix JS packages * update: add model onOpen to docs * feat(cli): add @docsearch/cli for MCP setup and search (#2911) * chore(tsdown): Bump to latest tsdown version (#2918) * chore(tsdown): Bump to latest tsdown version * fix: bump nvmrc node version * fix: cli tsconfig * fix: website build * fix: example build * fix: circleci install bun * fix: lint * fix: circleci install bun * fix: circleci install bun * fix: circleci install bun * refactor(docusaurus-adapter): rework theme config for v5 and modularize SearchPage (#2904) Co-authored-by: Paul Jankowski <8BitTitan@gmail.com> * feat(askai): Move askai related props under root askai (#2919) * feat(askai): Move askai related props under root askai * fix: playwright test case * fix(docusaurus): validate Ask AI options * feat(js): Split JS bundles for search only (#2920) * chore: Move to oxlint and oxfmt (#2923) * chore: Get NPM OIDC token before publishing (#2924) * chore: Enter v5 beta (#2925) * chore: Enter pre release mode for v5 * chore: update release summary * chore: version bump * fix: Remove NPM_ID_TOKEN for release * fix: Try setting blank NPM_TOKEN * fix: Try blank NPM_AUTH_TOKEN * chore: bump node and npm for release job * docs(mcp): add service disclaimer (#2921) * fix: Agent Studio MCP search tool (#2927) * fix: Agent Studio MCP search tool * Add changeset * chore: Update stylelint (#2926) * chore: stylelint update * bun.lock * Add changeset * fix(website): use bare @import for tailwindcss (#2933) Tailwind's build-time `@import` cannot be written with `url()` notation, so `@import url('tailwindcss')` was passed through as a plain CSS import instead of being processed by Tailwind. Also syncs bun.lock with the 5.0.0-beta.0 versions already committed to package.json. * chore: version v5.0.0-beta.1 (#2932) * feat(react): remove deprecated index props (#2936) * feat(mcp): add ChatGPT and Codex DocSearch plugin package (#2938) * fix: cleanup claude * feat: website redesign (#2930) Co-authored-by: Cursor <cursoragent@cursor.com> * fix: lint * fix: crash on the demo (#2940) * feat(docs): Document v5 beta (#2935) * chore(docs): v5 documentation * Writing style clean up * fix: website after conflicts * fix: reported issues on mobile (#2944) * chore: Introduce a11y smoke tests (#2943) * chore: Add Lorris as codeowner (#2946) * fix(askai): Ask AI fixes for v5 (#2945) * fix: General v5 fixes (#2947) - Fix `ref` console error for a `FacetMenu` - Whitespace only search/conversation input does not trigger requests - Fix flash of no results page on search * feat: v5 general improvements (#2948) * feat(v5): General fixes and improvements * add changeset * fix: bundlesize * feat(v5): UI and DX improvements (#2949) * feat: Rename assistantId to agentId * feat: Allow reading default facet values from index searchParameters * feat: Remove indexName prop from Sidepanel, cleanup documentation pages * feat: Move appId and apiKey up into @docsearch/core * feat: Add back nested grouping of search results * add changeset * revert changes to example demo * fix: e2e tests * chore: push git tags on version release (#2951) * chore: release v5.0.0-beta.2 (#2950) * chore: Fix pushing git tags (#2953) * fix(askai): sanitize markdown HTML in v5 (#2954) Backport of #2929.\n\nOriginal commit:681cbfec03Co-authored-by: Vasco Bettencourt <32492444+vascobettencourt@users.noreply.github.com> * fix(v5): stop truncating mobile snippets (#2958) * fix(v5): stop truncating mobile snippets Backport of #2907.\n\nOriginal commit:9ad6d169fe* fix(v5): allow mobile hit text to wrap Completes the v5 adaptation of #2907 by overriding later v5 child-level truncation rules.\n\nOriginal commit:9ad6d169fe* chore(v5): account for mobile wrapping CSS Updates the CSS size budget for the v5 adaptation of #2907.\n\nOriginal commit:9ad6d169fe--------- Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> * feat(v5): Add new footerAction prop (#2952) * feat(v5): Add new footerAction prop * Resolve PR comments * fix(v5): recognize conversation depth errors (#2957) Backport of #2881.\n\nOriginal commit:f68e52251cCo-authored-by: Felipe Bermudez <felipeberm@gmail.com> * fix(v5): expose Sidepanel search parameter types (#2956) * fix(v5): expose Sidepanel search parameter types Backport of #2906.\n\nOriginal commit:4710d0ca77* Delete sidepanel.test.ts Had a pointless test case in it. --------- Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> * fix(v5): ignore slash shortcut on focused buttons (#2955) Backport of #2871.\n\nOriginal commit:0e41a78c44Co-authored-by: Sigmabro <122412346+Sigmabrogz@users.noreply.github.com> * fix(agentStudio): agents dynamic mode enabled (#2959) * fix(agentStudio): agents dynamic mode enabled * fix(askai): use string[] for dynamic agentStudio indices * feat(docs): add Ask AI to Agent Studio migration guide (#2931) * feat(docs): add Ask AI to Agent Studio migration guide * feat(docs): agentStudio migrating from askAI * feat(docs): renaming agentId * feat(agentStudio): dynamic mode indices updated * fix: Docusaurus adapter styling, DocSearch website fixes (#2960) * chore: release v5.0.0-beta.3 (#2961) * feat(website): Launch updates (#2964) * fix(website): Fix font loading (#2966) * feat(v5): Back port cost control errors (#2965) * chore: release v5.0.0-beta.4 (#2967) --------- Co-authored-by: Vincent Lemeunier <vincentlemeunier+git@gmail.com> Co-authored-by: Dylan Tientcheu <dylan.tientcheu@algolia.com> Co-authored-by: Lorris Saint-Genez <lorrissaintgenez@gmail.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Dylan Tientcheu <dylantientcheu@gmail.com> Co-authored-by: Vasco Bettencourt <32492444+vascobettencourt@users.noreply.github.com> Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Co-authored-by: Felipe Bermudez <felipeberm@gmail.com> Co-authored-by: Sigmabro <122412346+Sigmabrogz@users.noreply.github.com>
This commit is contained in:
parent
9378e83941
commit
ecd905d440
586 changed files with 49203 additions and 41727 deletions
18
.changeset/README.md
Normal file
18
.changeset/README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Changesets
|
||||||
|
|
||||||
|
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in the changesets repo](https://github.com/changesets/changesets).
|
||||||
|
|
||||||
|
## How to contribute a changeset
|
||||||
|
|
||||||
|
Before merging your PR, you should add a changeset if your change affects any of the published packages.
|
||||||
|
|
||||||
|
1. Run `bun run changeset` in the root of the repository
|
||||||
|
2. Select the packages that are affected by your change
|
||||||
|
3. Choose the type of version bump (major, minor, or patch)
|
||||||
|
4. Write a summary of your changes that will be included in the changelog
|
||||||
|
|
||||||
|
All published `@docsearch/*` packages use a single version (fixed versioning). This means when one package is bumped, all packages get the same version.
|
||||||
|
|
||||||
|
## Releasing
|
||||||
|
|
||||||
|
When changesets are present in the `main` branch, the CI will create a "Version Packages" PR that bumps versions and updates changelogs. Merging that PR will trigger the publish workflow.
|
||||||
30
.changeset/better-cars-smell.md
Normal file
30
.changeset/better-cars-smell.md
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
"@docsearch/docusaurus-adapter": patch
|
||||||
|
"@docsearch/react": patch
|
||||||
|
"@docsearch/css": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(v5): General UI styling updates and fixes
|
||||||
|
|
||||||
|
- New `--docsearch-font-family` variable, used by the search button, keyboard
|
||||||
|
keys, modal, and sidepanel. It replaces the system font stacks that were
|
||||||
|
duplicated across `sidepanel.css` and `button.css`, so overriding one
|
||||||
|
variable now themes every DocSearch surface
|
||||||
|
- The search input and modal heading are `1rem` at every breakpoint
|
||||||
|
(previously `0.875rem` with a mobile-only override)
|
||||||
|
- `.DocSearch-Title` uses `line-height: 1.5em` instead of `0.5em` and adds
|
||||||
|
`overflow-wrap: anywhere`, so long titles wrap instead of overlapping (#2908)
|
||||||
|
- Hit icon `svg` sizing moved into `.DocSearch-Hit-icon`. The
|
||||||
|
`.DocSearch-Hit-icon--small` modifier is replaced by
|
||||||
|
`.DocSearch-Hit-icon--start`, which top-aligns the icon; recent
|
||||||
|
conversations use it
|
||||||
|
- The Ask AI button icon centers with `display: inline-flex` instead of
|
||||||
|
`margin-block-start`/`align-self` overrides
|
||||||
|
- Removed the 2px offset on the Ask AI sources action text
|
||||||
|
- `SourcesPanel` accepts `pluralTitleText`. `titleText` is now the singular
|
||||||
|
label, and the trigger renders `{count} {label}`
|
||||||
|
- `AskAiScreenTranslations` and `ConversationScreenTranslations` expose
|
||||||
|
`relatedSourcesTextPlural`
|
||||||
|
- Docusaurus adapter: `theme.SearchModal.askAiScreen.relatedSourcesText` is
|
||||||
|
now the singular "Source", and the new
|
||||||
|
`theme.SearchModal.askAiScreen.relatedSourcesTextPlural` provides "Sources"
|
||||||
15
.changeset/brave-things-join.md
Normal file
15
.changeset/brave-things-join.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
"@docsearch/docusaurus-adapter": patch
|
||||||
|
"@docsearch/sidepanel": patch
|
||||||
|
"@docsearch/modal": patch
|
||||||
|
"@docsearch/react": patch
|
||||||
|
"@docsearch/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(v5): UI and DX updates
|
||||||
|
|
||||||
|
- Rename the Ask AI assistantId option to agentId (adapter theme.SearchModal.askAi.assistantId → agentId)
|
||||||
|
- appId and apiKey moved up into @docsearch/core, so they're configured once and shared
|
||||||
|
- Removed the indexName prop from the Sidepanel
|
||||||
|
- Facet defaults can now be read from the index searchParameters
|
||||||
|
- Restored nested grouping of search results
|
||||||
6
.changeset/bright-errors-block.md
Normal file
6
.changeset/bright-errors-block.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
'@docsearch/css': patch
|
||||||
|
'@docsearch/react': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Surface Agent Studio cost-control errors and block prompts until the user can recover. [#2878](https://github.com/algolia/docsearch/pull/2878)
|
||||||
6
.changeset/calm-buttons-listen.md
Normal file
6
.changeset/calm-buttons-listen.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
"@docsearch/core": patch
|
||||||
|
"@docsearch/react": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Prevent the slash search shortcut from intercepting key events on focused buttons.
|
||||||
26
.changeset/config.json
Normal file
26
.changeset/config.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
||||||
|
"changelog": "@changesets/cli/changelog",
|
||||||
|
"commit": false,
|
||||||
|
"fixed": [
|
||||||
|
[
|
||||||
|
"@docsearch/css",
|
||||||
|
"@docsearch/core",
|
||||||
|
"@docsearch/js",
|
||||||
|
"@docsearch/modal",
|
||||||
|
"@docsearch/react",
|
||||||
|
"@docsearch/sidepanel",
|
||||||
|
"@docsearch/sidepanel-js",
|
||||||
|
"@docsearch/docusaurus-adapter"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"linked": [],
|
||||||
|
"access": "public",
|
||||||
|
"baseBranch": "v5",
|
||||||
|
"updateInternalDependencies": "patch",
|
||||||
|
"ignore": [
|
||||||
|
"@docsearch/website",
|
||||||
|
"@docsearch/react-example",
|
||||||
|
"@docsearch/js-example"
|
||||||
|
]
|
||||||
|
}
|
||||||
5
.changeset/deep-conversations-start.md
Normal file
5
.changeset/deep-conversations-start.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@docsearch/react": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Recognize current Agent Studio conversation-depth error messages.
|
||||||
6
.changeset/dynamic-indices-strings.md
Normal file
6
.changeset/dynamic-indices-strings.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
"@docsearch/react": patch
|
||||||
|
"@docsearch/docusaurus-adapter": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Align Ask AI dynamic indices with Agent Studio completions: `askAi.indices` is now `string[]` (index names only). Use `askAi.searchParameters` for per-index runtime overrides.
|
||||||
5
.changeset/kind-foxes-give.md
Normal file
5
.changeset/kind-foxes-give.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@docsearch/docusaurus-adapter": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(docusaurus-adapter): Docusaurus styling cleanup
|
||||||
6
.changeset/long-snippets-wrap.md
Normal file
6
.changeset/long-snippets-wrap.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
"@docsearch/react": patch
|
||||||
|
"@docsearch/css": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Stop over-truncating mobile snippets and allow long search hits to wrap.
|
||||||
35
.changeset/pre.json
Normal file
35
.changeset/pre.json
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"mode": "pre",
|
||||||
|
"tag": "beta",
|
||||||
|
"initialVersions": {
|
||||||
|
"@docsearch/docusaurus-adapter": "4.6.0",
|
||||||
|
"@docsearch/js-example": "4.6.0",
|
||||||
|
"@docsearch/react-example": "4.6.0",
|
||||||
|
"@docsearch/cli": "0.0.2",
|
||||||
|
"@docsearch/core": "4.6.0",
|
||||||
|
"@docsearch/css": "4.6.0",
|
||||||
|
"@docsearch/js": "4.6.0",
|
||||||
|
"@docsearch/modal": "4.6.0",
|
||||||
|
"@docsearch/react": "4.6.0",
|
||||||
|
"@docsearch/sidepanel": "4.6.0",
|
||||||
|
"@docsearch/sidepanel-js": "4.6.0",
|
||||||
|
"@docsearch/website": "4.6.0"
|
||||||
|
},
|
||||||
|
"changesets": [
|
||||||
|
"better-cars-smell",
|
||||||
|
"brave-things-join",
|
||||||
|
"bright-errors-block",
|
||||||
|
"calm-buttons-listen",
|
||||||
|
"deep-conversations-start",
|
||||||
|
"dynamic-indices-strings",
|
||||||
|
"kind-foxes-give",
|
||||||
|
"long-snippets-wrap",
|
||||||
|
"ready-clubs-serve",
|
||||||
|
"safe-answers-smile",
|
||||||
|
"sharp-donkeys-search",
|
||||||
|
"sixty-lights-try",
|
||||||
|
"tidy-moons-argue",
|
||||||
|
"typed-panels-wave",
|
||||||
|
"vast-numbers-read"
|
||||||
|
]
|
||||||
|
}
|
||||||
20
.changeset/ready-clubs-serve.md
Normal file
20
.changeset/ready-clubs-serve.md
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
"@docsearch/react": patch
|
||||||
|
"@docsearch/css": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(askai): Ask AI fixes for v5
|
||||||
|
|
||||||
|
- `getMessageContent` now joins every assistant text part instead of stopping
|
||||||
|
at the first one, so copying an answer that interleaves text with tool calls
|
||||||
|
returns the complete response (#2782)
|
||||||
|
- Key Ask AI conversation storage by `appId` instead of `indexName`, fixes
|
||||||
|
triggering a new conversation in hybrid mode
|
||||||
|
- Negative feedback panel is labelled with `aria-labelledby` and reason chips
|
||||||
|
expose selection via `aria-pressed` (styling moved off the
|
||||||
|
`--selected` modifier)
|
||||||
|
- `Popover.Trigger` forwards refs, fixes sources panel not showing in some cases
|
||||||
|
- Add `--docsearch-error-soft-color`, `--docsearch-error-text-color`, and
|
||||||
|
`--docsearch-code-block-background` variables; restyle the Ask AI error panel
|
||||||
|
and markdown code blocks to use them
|
||||||
|
```
|
||||||
5
.changeset/safe-answers-smile.md
Normal file
5
.changeset/safe-answers-smile.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@docsearch/react": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Sanitize Ask AI Markdown HTML and URLs before rendering to prevent XSS.
|
||||||
20
.changeset/sharp-donkeys-search.md
Normal file
20
.changeset/sharp-donkeys-search.md
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
"@docsearch/react": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(askai): support Agent Studio's batched MCP search tool
|
||||||
|
|
||||||
|
The Algolia MCP search tool can now issue multiple queries in a single tool
|
||||||
|
call (`queries[]`), which previously rendered as an empty query and broke
|
||||||
|
consecutive tool-call aggregation.
|
||||||
|
|
||||||
|
- `AlgoliaMCPSearchTool["input"]` accepts both the legacy single-query shape
|
||||||
|
and the new batched `queries[]` shape, and tolerates a missing `output`
|
||||||
|
- New `getSearchToolQueries()` helper normalizes query extraction across
|
||||||
|
`searchIndex`, batched MCP, and legacy MCP tool parts
|
||||||
|
- `ToolCall` renders one tool state per query for both `input-available` and
|
||||||
|
`output-available` states
|
||||||
|
- `groupConsecutiveToolResults` reuses the shared helper so batched calls
|
||||||
|
aggregate correctly
|
||||||
|
- Fix `AggregatedSearchBlock` keyboard handler comparing `e.key === 'enter'`,
|
||||||
|
which never matched and made query chips unusable via keyboard
|
||||||
14
.changeset/sixty-lights-try.md
Normal file
14
.changeset/sixty-lights-try.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
"@docsearch/react": patch
|
||||||
|
"@docsearch/css": patch
|
||||||
|
"@docsearch/js": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(v5): add customizable footer action
|
||||||
|
|
||||||
|
- New `footerAction` prop renders a custom action in the modal footer,
|
||||||
|
before the Algolia logo, inside `.DocSearch-Footer-Action`
|
||||||
|
- `@docsearch/js` supports `footerAction` via template patterns (html helper,
|
||||||
|
JSX, or function-based)
|
||||||
|
- Fixes typing differences between `@docsearch/react` and `@docsearch/js`
|
||||||
|
- Restyle the footer with a `.DocSearch-Footer-Actions` wrapper
|
||||||
20
.changeset/tidy-moons-argue.md
Normal file
20
.changeset/tidy-moons-argue.md
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
"@docsearch/docusaurus-adapter": patch
|
||||||
|
"@docsearch/css": patch
|
||||||
|
"@docsearch/react": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Align CSS class and animation names with the `DocSearch-` naming convention.
|
||||||
|
|
||||||
|
- The `shimmer` utility class is now `DocSearch-shimmer`
|
||||||
|
- Fixed a typo in `DocSearck-AskAiScreen-MessageContent-Stopped`, now
|
||||||
|
`DocSearch-AskAiScreen-MessageContent-Stopped`
|
||||||
|
- Keyframes renamed to kebab-case: `shimmerText` → `shimmer-text`,
|
||||||
|
`slideDown` → `slide-down`, `fadeIn` → `fade-in`
|
||||||
|
- Dark theme selectors use `:root[data-theme='dark']` instead of
|
||||||
|
`html[data-theme='dark']`
|
||||||
|
- Dropped redundant type qualifiers from `.DocSearch-Hits-padded` and
|
||||||
|
`.DocSearch-Hit-Select-Icon` selectors, slightly lowering their specificity
|
||||||
|
|
||||||
|
If you override these classes or keyframes in custom styles, update your
|
||||||
|
selectors accordingly.
|
||||||
5
.changeset/typed-panels-wave.md
Normal file
5
.changeset/typed-panels-wave.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@docsearch/sidepanel-js": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Expose Agent Studio search parameters in the public Sidepanel JavaScript props.
|
||||||
46
.changeset/vast-numbers-read.md
Normal file
46
.changeset/vast-numbers-read.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
---
|
||||||
|
"@docsearch/docusaurus-adapter": major
|
||||||
|
"@docsearch/core": major
|
||||||
|
"@docsearch/css": major
|
||||||
|
"@docsearch/js": major
|
||||||
|
"@docsearch/modal": major
|
||||||
|
"@docsearch/react": major
|
||||||
|
"@docsearch/sidepanel": major
|
||||||
|
"@docsearch/sidepanel-js": major
|
||||||
|
---
|
||||||
|
|
||||||
|
# DocSearch v5-beta
|
||||||
|
|
||||||
|
DocSearch v5 is a major release that introduces AI-powered answers via Agent
|
||||||
|
Studio, a refreshed search UI, and a modernized package architecture.
|
||||||
|
|
||||||
|
## Ask AI & Agent Studio
|
||||||
|
|
||||||
|
- Agent Studio integration with core tools and dynamic tool calls
|
||||||
|
- Conversation memory support
|
||||||
|
- Compatibility with the Algolia MCP search tool, with aggregated MCP search
|
||||||
|
tool calls
|
||||||
|
- Dynamic index selection for Agent Studio
|
||||||
|
- Feedback integration, including feedback notes and tags
|
||||||
|
- Prompt suggestions in keyword search and follow-up prompt suggestions
|
||||||
|
- Ask AI modal split into its own component and Ask AI transport layer removed
|
||||||
|
|
||||||
|
## Search UI
|
||||||
|
|
||||||
|
- Refreshed v5 UI with improved dark theme, sources panel, and accessibility
|
||||||
|
- Faceted search with filter chips
|
||||||
|
- Hit breadcrumbs and result badges
|
||||||
|
|
||||||
|
## Packaging & architecture
|
||||||
|
|
||||||
|
- New `@docsearch/cli` package for MCP setup and search
|
||||||
|
- MCP plugin support
|
||||||
|
- Split JS bundles for search-only usage and JS-based hybrid mode
|
||||||
|
- Migrated the build system to tsdown
|
||||||
|
- Migrated CSS building to LightningCSS
|
||||||
|
|
||||||
|
## Breaking changes
|
||||||
|
|
||||||
|
- Ask AI related props are now nested under a single root `askai` option
|
||||||
|
- The Ask AI transport layer has been removed
|
||||||
|
|
||||||
|
|
@ -1,21 +1,26 @@
|
||||||
version: 2.1
|
version: 2.1
|
||||||
|
|
||||||
aliases:
|
aliases:
|
||||||
- &restore_yarn_cache
|
- &install_bun
|
||||||
name: Restore Yarn cache
|
name: Install Bun
|
||||||
|
command: |
|
||||||
|
npm install --global --prefix "$HOME/.local" bun@1.3.10
|
||||||
|
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$BASH_ENV"
|
||||||
|
|
||||||
|
- &restore_bun_cache
|
||||||
|
name: Restore Bun cache
|
||||||
keys:
|
keys:
|
||||||
- yarn-packages-{{ checksum "yarn.lock" }}
|
- bun-packages-{{ checksum "bun.lock" }}
|
||||||
|
|
||||||
- &save_yarn_cache
|
- &save_bun_cache
|
||||||
name: Save Yarn cache
|
name: Save Bun cache
|
||||||
key: yarn-packages-{{ checksum "yarn.lock" }}
|
key: bun-packages-{{ checksum "bun.lock" }}
|
||||||
paths:
|
paths:
|
||||||
- .yarn/cache
|
- ~/.bun/install/cache
|
||||||
- .yarn/unplugged
|
|
||||||
|
|
||||||
- &run_yarn_install
|
- &run_bun_install
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
command: yarn install --immutable
|
command: bun install --frozen-lockfile
|
||||||
|
|
||||||
- &restore_dist_folders
|
- &restore_dist_folders
|
||||||
name: Restore dist folders
|
name: Restore dist folders
|
||||||
|
|
@ -29,6 +34,8 @@ aliases:
|
||||||
mkdir -p packages/docsearch-modal/dist
|
mkdir -p packages/docsearch-modal/dist
|
||||||
mkdir -p packages/docsearch-sidepanel/dist
|
mkdir -p packages/docsearch-sidepanel/dist
|
||||||
mkdir -p packages/docsearch-sidepanel-js/dist
|
mkdir -p packages/docsearch-sidepanel-js/dist
|
||||||
|
mkdir -p packages/docsearch-cli/dist
|
||||||
|
mkdir -p adapters/docusaurus-theme-search-algolia/lib
|
||||||
|
|
||||||
cp -R /tmp/workspace/packages/docsearch-react/dist packages/docsearch-react
|
cp -R /tmp/workspace/packages/docsearch-react/dist packages/docsearch-react
|
||||||
cp -R /tmp/workspace/packages/docsearch-js/dist packages/docsearch-js
|
cp -R /tmp/workspace/packages/docsearch-js/dist packages/docsearch-js
|
||||||
|
|
@ -37,16 +44,18 @@ aliases:
|
||||||
cp -R /tmp/workspace/packages/docsearch-modal/dist packages/docsearch-modal
|
cp -R /tmp/workspace/packages/docsearch-modal/dist packages/docsearch-modal
|
||||||
cp -R /tmp/workspace/packages/docsearch-sidepanel/dist packages/docsearch-sidepanel
|
cp -R /tmp/workspace/packages/docsearch-sidepanel/dist packages/docsearch-sidepanel
|
||||||
cp -R /tmp/workspace/packages/docsearch-sidepanel-js/dist packages/docsearch-sidepanel-js
|
cp -R /tmp/workspace/packages/docsearch-sidepanel-js/dist packages/docsearch-sidepanel-js
|
||||||
|
cp -R /tmp/workspace/packages/docsearch-cli/dist packages/docsearch-cli
|
||||||
|
cp -R /tmp/workspace/adapters/docusaurus-theme-search-algolia/lib adapters/docusaurus-theme-search-algolia
|
||||||
|
|
||||||
defaults: &defaults
|
defaults: &defaults
|
||||||
working_directory: ~/docsearch
|
working_directory: ~/docsearch
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:22.15.0
|
- image: cimg/node:24.13.1
|
||||||
|
|
||||||
cypress: &cypress
|
playwright: &playwright
|
||||||
working_directory: ~/docsearch
|
working_directory: ~/docsearch
|
||||||
docker:
|
docker:
|
||||||
- image: cypress/browsers:node-22.15.0-chrome-136.0.7103.113-1-ff-138.0.3-edge-136.0.3240.64-1
|
- image: mcr.microsoft.com/playwright:v1.49.1-noble
|
||||||
environment:
|
environment:
|
||||||
## this enables colors in the output
|
## this enables colors in the output
|
||||||
TERM: xterm
|
TERM: xterm
|
||||||
|
|
@ -62,13 +71,15 @@ jobs:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- restore_cache: *restore_yarn_cache
|
- run: *install_bun
|
||||||
- run: *run_yarn_install
|
- restore_cache: *restore_bun_cache
|
||||||
- save_cache: *save_yarn_cache
|
- run: *run_bun_install
|
||||||
|
- save_cache: *save_bun_cache
|
||||||
- run:
|
- run:
|
||||||
name: Build and Size
|
name: Build and Size
|
||||||
command: |
|
command: |
|
||||||
yarn run build
|
bun run build
|
||||||
|
bun run --filter @docsearch/cli test:pack
|
||||||
- run:
|
- run:
|
||||||
name: Move dist folders to workspace
|
name: Move dist folders to workspace
|
||||||
command: |
|
command: |
|
||||||
|
|
@ -81,6 +92,8 @@ jobs:
|
||||||
mkdir -p /tmp/workspace/packages/docsearch-modal/dist
|
mkdir -p /tmp/workspace/packages/docsearch-modal/dist
|
||||||
mkdir -p /tmp/workspace/packages/docsearch-sidepanel/dist
|
mkdir -p /tmp/workspace/packages/docsearch-sidepanel/dist
|
||||||
mkdir -p /tmp/workspace/packages/docsearch-sidepanel-js/dist
|
mkdir -p /tmp/workspace/packages/docsearch-sidepanel-js/dist
|
||||||
|
mkdir -p /tmp/workspace/packages/docsearch-cli/dist
|
||||||
|
mkdir -p /tmp/workspace/adapters/docusaurus-theme-search-algolia/lib
|
||||||
|
|
||||||
cp -R packages/docsearch-react/dist /tmp/workspace/packages/docsearch-react
|
cp -R packages/docsearch-react/dist /tmp/workspace/packages/docsearch-react
|
||||||
cp -R packages/docsearch-js/dist /tmp/workspace/packages/docsearch-js
|
cp -R packages/docsearch-js/dist /tmp/workspace/packages/docsearch-js
|
||||||
|
|
@ -89,66 +102,78 @@ jobs:
|
||||||
cp -R packages/docsearch-modal/dist /tmp/workspace/packages/docsearch-modal
|
cp -R packages/docsearch-modal/dist /tmp/workspace/packages/docsearch-modal
|
||||||
cp -R packages/docsearch-sidepanel/dist /tmp/workspace/packages/docsearch-sidepanel
|
cp -R packages/docsearch-sidepanel/dist /tmp/workspace/packages/docsearch-sidepanel
|
||||||
cp -R packages/docsearch-sidepanel-js/dist /tmp/workspace/packages/docsearch-sidepanel-js
|
cp -R packages/docsearch-sidepanel-js/dist /tmp/workspace/packages/docsearch-sidepanel-js
|
||||||
|
cp -R packages/docsearch-cli/dist /tmp/workspace/packages/docsearch-cli
|
||||||
|
cp -R adapters/docusaurus-theme-search-algolia/lib /tmp/workspace/adapters/docusaurus-theme-search-algolia
|
||||||
- persist_to_workspace:
|
- persist_to_workspace:
|
||||||
root: *workspace_root
|
root: *workspace_root
|
||||||
paths:
|
paths:
|
||||||
- packages
|
- packages
|
||||||
|
- adapters
|
||||||
test_lint:
|
test_lint:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- *attach_workspace
|
- *attach_workspace
|
||||||
- restore_cache: *restore_yarn_cache
|
- run: *install_bun
|
||||||
- run: *run_yarn_install
|
- restore_cache: *restore_bun_cache
|
||||||
- save_cache: *save_yarn_cache
|
- run: *run_bun_install
|
||||||
|
- save_cache: *save_bun_cache
|
||||||
- run: *restore_dist_folders
|
- run: *restore_dist_folders
|
||||||
- run:
|
- run:
|
||||||
name: Linting
|
name: Linting
|
||||||
command: yarn run lint
|
command: |
|
||||||
|
bun run lint --quiet --format=stylish
|
||||||
|
bun run lint:css
|
||||||
test_types:
|
test_types:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- *attach_workspace
|
- *attach_workspace
|
||||||
- restore_cache: *restore_yarn_cache
|
- run: *install_bun
|
||||||
- run: *run_yarn_install
|
- restore_cache: *restore_bun_cache
|
||||||
- save_cache: *save_yarn_cache
|
- run: *run_bun_install
|
||||||
|
- save_cache: *save_bun_cache
|
||||||
- run: *restore_dist_folders
|
- run: *restore_dist_folders
|
||||||
- run:
|
- run:
|
||||||
name: Type checking
|
name: Type checking
|
||||||
command: yarn run test:types
|
command: bun run test:types
|
||||||
test_size:
|
test_size:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- *attach_workspace
|
- *attach_workspace
|
||||||
- restore_cache: *restore_yarn_cache
|
- run: *install_bun
|
||||||
- run: *run_yarn_install
|
- restore_cache: *restore_bun_cache
|
||||||
- save_cache: *save_yarn_cache
|
- run: *run_bun_install
|
||||||
|
- save_cache: *save_bun_cache
|
||||||
- run: *restore_dist_folders
|
- run: *restore_dist_folders
|
||||||
- run:
|
- run:
|
||||||
name: Size checking
|
name: Size checking
|
||||||
command: yarn run test:size
|
command: bun run test:size
|
||||||
test_unit:
|
test_unit:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- *attach_workspace
|
- *attach_workspace
|
||||||
- restore_cache: *restore_yarn_cache
|
- run: *install_bun
|
||||||
- run: *run_yarn_install
|
- restore_cache: *restore_bun_cache
|
||||||
- save_cache: *save_yarn_cache
|
- run: *run_bun_install
|
||||||
|
- save_cache: *save_bun_cache
|
||||||
- run: *restore_dist_folders
|
- run: *restore_dist_folders
|
||||||
- run:
|
- run:
|
||||||
name: Unit tests
|
name: Unit tests
|
||||||
command: yarn run test
|
command: bun run test
|
||||||
release:
|
release:
|
||||||
<<: *defaults
|
working_directory: ~/docsearch
|
||||||
|
docker:
|
||||||
|
- image: cimg/node:24.16
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- *attach_workspace
|
- *attach_workspace
|
||||||
- restore_cache: *restore_yarn_cache
|
- run: *install_bun
|
||||||
- run: *run_yarn_install
|
- restore_cache: *restore_bun_cache
|
||||||
- save_cache: *save_yarn_cache
|
- run: *run_bun_install
|
||||||
|
- save_cache: *save_bun_cache
|
||||||
- run: *restore_dist_folders
|
- run: *restore_dist_folders
|
||||||
- run:
|
- run:
|
||||||
name: Setup git user
|
name: Setup git user
|
||||||
|
|
@ -157,19 +182,29 @@ jobs:
|
||||||
git config --global user.name "algolia-bot"
|
git config --global user.name "algolia-bot"
|
||||||
- run:
|
- run:
|
||||||
name: Release if needed
|
name: Release if needed
|
||||||
command: yarn run shipjs trigger
|
command: |
|
||||||
test_cypress:
|
export NPM_ID_TOKEN="$(circleci run oidc get --claims '{"aud": "npm:registry.npmjs.org"}')"
|
||||||
<<: *cypress
|
bun run release
|
||||||
|
git push --follow-tags origin "HEAD:${CIRCLE_BRANCH}"
|
||||||
|
test_playwright:
|
||||||
|
<<: *playwright
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- *attach_workspace
|
- *attach_workspace
|
||||||
- restore_cache: *restore_yarn_cache
|
- run: *install_bun
|
||||||
- run: *run_yarn_install
|
- restore_cache: *restore_bun_cache
|
||||||
- save_cache: *save_yarn_cache
|
- run: *run_bun_install
|
||||||
|
- save_cache: *save_bun_cache
|
||||||
- run: *restore_dist_folders
|
- run: *restore_dist_folders
|
||||||
- run:
|
- run:
|
||||||
name: Cypress test Actions
|
name: Playwright E2E tests
|
||||||
command: yarn run cy:run
|
command: bun run pw:run
|
||||||
|
- store_artifacts:
|
||||||
|
path: playwright-report
|
||||||
|
destination: playwright-report
|
||||||
|
- store_artifacts:
|
||||||
|
path: test-results
|
||||||
|
destination: test-results
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
|
|
@ -188,7 +223,7 @@ workflows:
|
||||||
- test_unit:
|
- test_unit:
|
||||||
requires:
|
requires:
|
||||||
- build
|
- build
|
||||||
- test_cypress:
|
- test_playwright:
|
||||||
requires:
|
requires:
|
||||||
- build
|
- build
|
||||||
- release:
|
- release:
|
||||||
|
|
|
||||||
8
.cursor/mcp.json
Normal file
8
.cursor/mcp.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"algolia-docsearch": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://mcp.algolia.com/1/docsearch/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
.cursor/rules/use-algolia-docsearch.mdc
Symbolic link
1
.cursor/rules/use-algolia-docsearch.mdc
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
../../mcp/plugins/docsearch/cursor/algolia-docsearch/rules/use-algolia-docsearch.mdc
|
||||||
1
.cursor/settings.json
Normal file
1
.cursor/settings.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
GITHUB_TOKEN=
|
GITHUB_TOKEN=
|
||||||
NPM_AUTH_TOKEN=
|
|
||||||
CYPRESS_RECORD_KEY=
|
CYPRESS_RECORD_KEY=
|
||||||
PERCY_TOKEN=
|
PERCY_TOKEN=
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -21,9 +21,9 @@ dist/
|
||||||
# Environment files
|
# Environment files
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# Cypress Video and Screenshots output
|
# Playwright output
|
||||||
cypress/screenshots/
|
playwright-report/
|
||||||
cypress/videos/
|
test-results/
|
||||||
|
|
||||||
**/.yarn/*
|
**/.yarn/*
|
||||||
!**/.yarn/releases
|
!**/.yarn/releases
|
||||||
|
|
|
||||||
2
.nvmrc
2
.nvmrc
|
|
@ -1 +1 @@
|
||||||
22.15.0
|
24.13.1
|
||||||
|
|
|
||||||
23
.oxfmtrc.json
Normal file
23
.oxfmtrc.json
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
|
"proseWrap": "never",
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 80,
|
||||||
|
"sortPackageJson": false,
|
||||||
|
"sortImports": {
|
||||||
|
"groups": ["builtin", "external", "parent", "sibling", "index"],
|
||||||
|
"newlinesBetween": true
|
||||||
|
},
|
||||||
|
"jsdoc": true,
|
||||||
|
"ignorePatterns": [
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"build",
|
||||||
|
".cache",
|
||||||
|
"**/*.mdx",
|
||||||
|
"**/*.md",
|
||||||
|
"adapters/docusaurus-theme-search-algolia/lib",
|
||||||
|
"!adapters/docusaurus-theme-search-algolia/lib/theme/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
341
.oxlintrc.json
Normal file
341
.oxlintrc.json
Normal file
|
|
@ -0,0 +1,341 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": [
|
||||||
|
"import",
|
||||||
|
"jsdoc",
|
||||||
|
"unicorn",
|
||||||
|
"react",
|
||||||
|
"jsx-a11y",
|
||||||
|
"typescript",
|
||||||
|
"react-perf",
|
||||||
|
"oxc",
|
||||||
|
"eslint",
|
||||||
|
"vitest"
|
||||||
|
],
|
||||||
|
"jsPlugins": [
|
||||||
|
"@stylistic/eslint-plugin",
|
||||||
|
"@eslint-community/eslint-plugin-eslint-comments"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"builtin": true,
|
||||||
|
"es2026": true,
|
||||||
|
"commonjs": true
|
||||||
|
},
|
||||||
|
"ignorePatterns": [
|
||||||
|
"**/node_modules/",
|
||||||
|
"**/dist/",
|
||||||
|
"**/build/",
|
||||||
|
"**/.docusaurus",
|
||||||
|
"adapters/**/lib/**",
|
||||||
|
"examples/**/*"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"react/react-compiler": "warn",
|
||||||
|
"constructor-super": ["error"],
|
||||||
|
"for-direction": "error",
|
||||||
|
"getter-return": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowImplicit": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-console": ["error"],
|
||||||
|
"no-promise-executor-return": ["error"],
|
||||||
|
"no-template-curly-in-string": ["error"],
|
||||||
|
"accessor-pairs": ["error"],
|
||||||
|
"array-callback-return": ["error"],
|
||||||
|
"block-scoped-var": ["error"],
|
||||||
|
"default-case": ["error"],
|
||||||
|
"default-case-last": ["error"],
|
||||||
|
"default-param-last": ["error"],
|
||||||
|
"eqeqeq": ["error"],
|
||||||
|
"grouped-accessor-pairs": ["error"],
|
||||||
|
"guard-for-in": ["error"],
|
||||||
|
"max-classes-per-file": ["warn", 1],
|
||||||
|
"no-alert": ["error"],
|
||||||
|
"no-caller": ["error"],
|
||||||
|
"no-constructor-return": ["error"],
|
||||||
|
"no-div-regex": ["error"],
|
||||||
|
"no-else-return": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowElseIf": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-eq-null": ["error"],
|
||||||
|
"no-eval": ["error"],
|
||||||
|
"no-extend-native": ["error"],
|
||||||
|
"no-extra-bind": ["error"],
|
||||||
|
"no-extra-label": ["error"],
|
||||||
|
"no-implicit-coercion": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"string": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-implicit-globals": ["error"],
|
||||||
|
"no-implied-eval": ["error"],
|
||||||
|
"no-iterator": ["error"],
|
||||||
|
"no-labels": ["error"],
|
||||||
|
"no-lone-blocks": ["error"],
|
||||||
|
"no-loop-func": ["error"],
|
||||||
|
"no-multi-assign": ["error"],
|
||||||
|
"no-multi-str": ["error"],
|
||||||
|
"no-new": ["error"],
|
||||||
|
"no-new-func": ["error"],
|
||||||
|
"no-new-wrappers": ["error"],
|
||||||
|
"no-proto": ["error"],
|
||||||
|
"no-return-assign": ["error"],
|
||||||
|
"no-script-url": ["error"],
|
||||||
|
"no-self-compare": ["error"],
|
||||||
|
"no-sequences": ["error"],
|
||||||
|
"no-throw-literal": ["error"],
|
||||||
|
"no-unmodified-loop-condition": ["error"],
|
||||||
|
"no-useless-call": ["error"],
|
||||||
|
"no-useless-concat": ["error"],
|
||||||
|
"no-useless-return": ["error"],
|
||||||
|
"no-void": ["error"],
|
||||||
|
"no-warning-comments": ["error"],
|
||||||
|
"prefer-promise-reject-errors": ["error"],
|
||||||
|
"radix": ["error"],
|
||||||
|
"require-await": ["error"],
|
||||||
|
"vars-on-top": ["error"],
|
||||||
|
"yoda": ["error"],
|
||||||
|
"no-label-var": ["error"],
|
||||||
|
"no-restricted-globals": ["error", "event"],
|
||||||
|
"no-shadow": ["error"],
|
||||||
|
"no-use-before-define": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"functions": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@stylistic/spaced-comment": ["error"],
|
||||||
|
"max-depth": ["error"],
|
||||||
|
"max-nested-callbacks": ["error"],
|
||||||
|
"max-params": ["error", 5],
|
||||||
|
"new-cap": ["error"],
|
||||||
|
"no-array-constructor": ["error"],
|
||||||
|
"no-bitwise": ["error"],
|
||||||
|
"no-lonely-if": ["error"],
|
||||||
|
"no-nested-ternary": ["error"],
|
||||||
|
"no-unneeded-ternary": ["error"],
|
||||||
|
"unicode-bom": ["error"],
|
||||||
|
"no-useless-computed-key": ["error"],
|
||||||
|
"no-useless-constructor": ["error"],
|
||||||
|
"no-useless-rename": ["error"],
|
||||||
|
"no-var": ["error"],
|
||||||
|
"object-shorthand": ["error"],
|
||||||
|
"prefer-const": ["error"],
|
||||||
|
"prefer-rest-params": ["error"],
|
||||||
|
"prefer-spread": ["error"],
|
||||||
|
"symbol-description": ["error"],
|
||||||
|
"@eslint-community/eslint-comments/disable-enable-pair": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowWholeFile": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@eslint-community/eslint-comments/no-aggregating-enable": ["error"],
|
||||||
|
"@eslint-community/eslint-comments/no-duplicate-disable": ["error"],
|
||||||
|
"@eslint-community/eslint-comments/no-unlimited-disable": ["error"],
|
||||||
|
"@eslint-community/eslint-comments/no-unused-disable": ["error"],
|
||||||
|
"@eslint-community/eslint-comments/no-unused-enable": ["error"],
|
||||||
|
"import/first": "error",
|
||||||
|
"import/newline-after-import": "error",
|
||||||
|
"import/no-amd": ["error"],
|
||||||
|
"import/no-commonjs": ["error"],
|
||||||
|
"import/no-duplicates": ["error"],
|
||||||
|
"import/no-named-as-default": "error",
|
||||||
|
"jsdoc/check-tag-names": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"definedTags": ["required", "swagger"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"jsdoc/implements-on-classes": ["error"],
|
||||||
|
"jsdoc/require-param-description": ["error"],
|
||||||
|
"jsdoc/require-param-name": ["error"],
|
||||||
|
"jsdoc/require-returns-description": ["error"],
|
||||||
|
"react/jsx-key": ["error"],
|
||||||
|
"react/jsx-no-comment-textnodes": "error",
|
||||||
|
"react/jsx-no-duplicate-props": "error",
|
||||||
|
"react/jsx-no-target-blank": ["error"],
|
||||||
|
"react/jsx-no-undef": "error",
|
||||||
|
"react/no-children-prop": "error",
|
||||||
|
"react/no-danger-with-children": "error",
|
||||||
|
"react/no-direct-mutation-state": ["error"],
|
||||||
|
"react/no-find-dom-node": ["error"],
|
||||||
|
"react/no-is-mounted": ["error"],
|
||||||
|
"react/no-render-return-value": "error",
|
||||||
|
"react/no-string-refs": ["error"],
|
||||||
|
"react/no-unknown-property": "error",
|
||||||
|
"react/react-in-jsx-scope": "error",
|
||||||
|
"react/button-has-type": ["error"],
|
||||||
|
"react/jsx-boolean-value": ["error", "always"],
|
||||||
|
"react/jsx-pascal-case": ["error"],
|
||||||
|
"react/jsx-fragments": ["error"],
|
||||||
|
"react/jsx-handler-names": ["error"],
|
||||||
|
"react/no-array-index-key": ["warn"],
|
||||||
|
"react/no-did-mount-set-state": ["warn"],
|
||||||
|
"react/no-did-update-set-state": ["warn"],
|
||||||
|
"react/no-multi-comp": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"ignoreStateless": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"react/no-redundant-should-component-update": ["error"],
|
||||||
|
"react/no-this-in-sfc": ["error"],
|
||||||
|
"react/self-closing-comp": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"html": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"jsx-a11y/alt-text": ["error"],
|
||||||
|
"jsx-a11y/anchor-is-valid": ["error"],
|
||||||
|
"jsx-a11y/anchor-has-content": "error",
|
||||||
|
"jsx-a11y/aria-activedescendant-has-tabindex": ["error"],
|
||||||
|
"jsx-a11y/aria-props": ["error"],
|
||||||
|
"jsx-a11y/aria-proptypes": ["error"],
|
||||||
|
"jsx-a11y/aria-role": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"ignoreNonDOM": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"jsx-a11y/html-has-lang": ["error"],
|
||||||
|
"jsx-a11y/iframe-has-title": ["error"],
|
||||||
|
"jsx-a11y/interactive-supports-focus": ["error"],
|
||||||
|
"jsx-a11y/lang": ["error"],
|
||||||
|
"jsx-a11y/media-has-caption": ["error"],
|
||||||
|
"jsx-a11y/mouse-events-have-key-events": ["error"],
|
||||||
|
"jsx-a11y/no-access-key": ["error"],
|
||||||
|
"jsx-a11y/no-autofocus": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"ignoreNonDOM": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"jsx-a11y/no-distracting-elements": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"elements": ["marquee", "blink"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"jsx-a11y/no-interactive-element-to-noninteractive-role": ["error"],
|
||||||
|
"jsx-a11y/no-noninteractive-element-interactions": ["error"],
|
||||||
|
"jsx-a11y/no-noninteractive-element-to-interactive-role": ["error"],
|
||||||
|
"jsx-a11y/no-noninteractive-tabindex": ["error"],
|
||||||
|
"jsx-a11y/no-redundant-roles": ["error"],
|
||||||
|
"jsx-a11y/no-static-element-interactions": ["error"],
|
||||||
|
"jsx-a11y/role-has-required-aria-props": ["error"],
|
||||||
|
"jsx-a11y/role-supports-aria-props": ["error"],
|
||||||
|
"jsx-a11y/scope": ["error"],
|
||||||
|
"jsx-a11y/tabindex-no-positive": ["error"],
|
||||||
|
"@stylistic/comma-spacing": ["error"],
|
||||||
|
"@stylistic/func-call-spacing": ["error"],
|
||||||
|
"@stylistic/type-annotation-spacing": ["error"],
|
||||||
|
"react/rules-of-hooks": "error",
|
||||||
|
"react/exhaustive-deps": "error",
|
||||||
|
"typescript/adjacent-overload-signatures": ["error"],
|
||||||
|
"typescript/array-type": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"default": "array-simple"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typescript/no-restricted-types": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"types": {
|
||||||
|
"String": {
|
||||||
|
"message": "Use `string` instead.",
|
||||||
|
"fixWith": "string"
|
||||||
|
},
|
||||||
|
"Number": {
|
||||||
|
"message": "Use `number` instead.",
|
||||||
|
"fixWith": "number"
|
||||||
|
},
|
||||||
|
"Boolean": {
|
||||||
|
"message": "Use `boolean` instead.",
|
||||||
|
"fixWith": "boolean"
|
||||||
|
},
|
||||||
|
"Symbol": {
|
||||||
|
"message": "Use `symbol` instead.",
|
||||||
|
"fixWith": "symbol"
|
||||||
|
},
|
||||||
|
"Object": {
|
||||||
|
"message": "The `Object` type is mostly the same as `unknown`. You probably want `Record<string, unknown>` instead. See https://github.com/typescript-eslint/typescript-eslint/pull/848",
|
||||||
|
"fixWith": "Record<string, unknown>"
|
||||||
|
},
|
||||||
|
"{}": {
|
||||||
|
"message": "The `{}` type is mostly the same as `unknown`. You probably want `Record<string, unknown>` instead.",
|
||||||
|
"fixWith": "Record<string, unknown>"
|
||||||
|
},
|
||||||
|
"object": {
|
||||||
|
"message": "The `object` type is hard to use. Use `Record<string, unknown>` instead. See: https://github.com/typescript-eslint/typescript-eslint/pull/848",
|
||||||
|
"fixWith": "Record<string, unknown>"
|
||||||
|
},
|
||||||
|
"Function": "Use a specific function type instead, like `() => void`."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typescript/consistent-type-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"prefer": "type-imports"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typescript/consistent-type-assertions": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"assertionStyle": "as",
|
||||||
|
"objectLiteralTypeAssertions": "allow-as-parameter"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typescript/no-empty-interface": ["error"],
|
||||||
|
"typescript/no-misused-new": ["error"],
|
||||||
|
"typescript/no-namespace": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowDeclarations": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typescript/triple-slash-reference": ["error"],
|
||||||
|
"typescript/no-confusing-non-null-assertion": ["error"],
|
||||||
|
"typescript/no-extra-non-null-assertion": ["error"],
|
||||||
|
"typescript/no-non-null-asserted-optional-chain": ["error"],
|
||||||
|
"typescript/no-unnecessary-type-constraint": ["error"],
|
||||||
|
"typescript/no-var-requires": ["error"],
|
||||||
|
"typescript/prefer-as-const": ["error"],
|
||||||
|
"typescript/prefer-enum-initializers": ["error"],
|
||||||
|
"typescript/prefer-function-type": ["error"],
|
||||||
|
"typescript/prefer-literal-enum-member": ["error"],
|
||||||
|
"typescript/prefer-namespace-keyword": ["error"],
|
||||||
|
"typescript/prefer-ts-expect-error": ["error"],
|
||||||
|
"typescript/unified-signatures": ["error"],
|
||||||
|
"vitest/require-mock-type-parameters": "off",
|
||||||
|
"no-debugger": ["error"]
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["**/*.ts", "**/*.tsx"],
|
||||||
|
"rules": {
|
||||||
|
"typescript/explicit-member-accessibility": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"accessibility": "no-public"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["packages/website/**/*"],
|
||||||
|
"rules": {
|
||||||
|
"react/react-compiler": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
build
|
|
||||||
.cache
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"proseWrap": "never",
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "es5"
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
{
|
|
||||||
"plugins": [
|
|
||||||
"stylelint-prettier",
|
|
||||||
"stylelint-no-unsupported-browser-features"
|
|
||||||
],
|
|
||||||
"extends": [
|
|
||||||
"stylelint-config-standard",
|
|
||||||
"stylelint-config-sass-guidelines",
|
|
||||||
"stylelint-order",
|
|
||||||
"stylelint-prettier/recommended"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"selector-class-pattern": [
|
|
||||||
"^DocSearch-[A-Za-z0-9-]*$"
|
|
||||||
],
|
|
||||||
"prettier/prettier": true,
|
|
||||||
"max-nesting-depth": [
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"ignore": [
|
|
||||||
"pseudo-classes"
|
|
||||||
],
|
|
||||||
"ignoreAtRules": [
|
|
||||||
"media"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"plugin/no-unsupported-browser-features": [
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
"severity": "warning"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2
.tool-versions
Normal file
2
.tool-versions
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
bun 1.3.10
|
||||||
|
nodejs 24.13.1
|
||||||
934
.yarn/releases/yarn-4.6.0.cjs
vendored
934
.yarn/releases/yarn-4.6.0.cjs
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,5 +0,0 @@
|
||||||
nodeLinker: node-modules
|
|
||||||
|
|
||||||
enableGlobalCache: false
|
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-4.6.0.cjs
|
|
||||||
102
AGENTS.md
102
AGENTS.md
|
|
@ -4,7 +4,7 @@ This document provides guidelines for AI agents working on the DocSearch codebas
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
DocSearch is an Algolia-powered search widget for documentation sites. It's a TypeScript/React monorepo using Yarn workspaces with Lerna for orchestration.
|
DocSearch is an Algolia-powered search widget for documentation sites. It's a TypeScript/React monorepo using Bun workspaces.
|
||||||
|
|
||||||
### Packages
|
### Packages
|
||||||
|
|
||||||
|
|
@ -21,98 +21,72 @@ DocSearch is an Algolia-powered search widget for documentation sites. It's a Ty
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
yarn install
|
bun install
|
||||||
|
|
||||||
# Build all packages
|
# Build all packages
|
||||||
yarn build
|
bun run build
|
||||||
|
|
||||||
# Build specific package
|
# Build specific package
|
||||||
yarn workspace @docsearch/react build
|
bun run --filter @docsearch/react build
|
||||||
|
|
||||||
# Watch mode (all packages)
|
# Watch mode (all packages)
|
||||||
yarn watch
|
bun run watch
|
||||||
|
|
||||||
# Clean builds
|
|
||||||
yarn build:clean
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Test Commands
|
## Test Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all unit tests
|
# Run all unit tests
|
||||||
yarn test
|
bun run test
|
||||||
|
|
||||||
# Run a single test file
|
# Run a single test file
|
||||||
yarn test packages/docsearch-react/src/__tests__/utils.test.ts
|
bun run test --run packages/docsearch-react/src/__tests__/utils.test.ts
|
||||||
|
|
||||||
# Run tests matching a pattern
|
|
||||||
yarn test --testNamePattern="extractLinksFromText"
|
|
||||||
|
|
||||||
# Run tests in watch mode
|
|
||||||
yarn test --watch
|
|
||||||
|
|
||||||
# Type checking
|
# Type checking
|
||||||
yarn test:types
|
bun run test:types
|
||||||
|
|
||||||
# Bundle size check
|
# Bundle size check
|
||||||
yarn test:size
|
bun run test:size
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When running tests, prefer to run specific files with the `--run` flag to prevent running with watch mode.
|
||||||
|
|
||||||
## Lint Commands
|
## Lint Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run ESLint
|
# Run oxlint
|
||||||
yarn lint
|
bun run lint --format=agent
|
||||||
|
|
||||||
# Run CSS linting
|
# Perform oxfmt formatting
|
||||||
yarn lint:css
|
bun run fmt
|
||||||
|
|
||||||
|
# Lint CSS
|
||||||
|
bun run lint:css
|
||||||
```
|
```
|
||||||
|
|
||||||
## E2E Testing (Cypress)
|
## E2E Testing (Playwright)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run Cypress tests
|
# Run Cypress tests
|
||||||
yarn cy:run
|
bun run pw:run
|
||||||
|
|
||||||
# Run with specific browser
|
# Run with specific browser
|
||||||
yarn cy:run:chrome
|
bun run pw:run:chromium
|
||||||
yarn cy:run:firefox
|
bun run pw:run:firefox
|
||||||
|
bun run pw:run:webkit
|
||||||
```
|
```
|
||||||
|
|
||||||
## Code Style Guidelines
|
## Code Style Guidelines
|
||||||
|
|
||||||
### Imports
|
|
||||||
|
|
||||||
Imports must be ordered alphabetically with newlines between groups:
|
|
||||||
|
|
||||||
1. Built-in modules
|
|
||||||
2. External dependencies
|
|
||||||
3. Parent directory imports
|
|
||||||
4. Sibling imports
|
|
||||||
5. Index imports
|
|
||||||
|
|
||||||
Internal `@/**/*` paths go before parent imports.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Correct order
|
|
||||||
import type { AutocompleteOptions } from '@algolia/autocomplete-core';
|
|
||||||
import React, { type JSX } from 'react';
|
|
||||||
|
|
||||||
import { DocSearchButton } from './DocSearchButton';
|
|
||||||
import type { DocSearchHit } from './types';
|
|
||||||
```
|
|
||||||
|
|
||||||
### TypeScript
|
### TypeScript
|
||||||
|
|
||||||
- Use `type` imports for type-only imports: `import type { Foo } from './types'`
|
- Use `type` imports for type-only imports: `import type { Foo } from './types'`
|
||||||
- Prefer interfaces for object shapes, types for unions/primitives
|
- Prefer interfaces for object shapes, types for unions/primitives
|
||||||
- Explicit return types on exported functions
|
|
||||||
- Use `declare type` for exported type declarations
|
|
||||||
- Avoid `any`; use `unknown` when type is truly unknown
|
- Avoid `any`; use `unknown` when type is truly unknown
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Good
|
// Good
|
||||||
export declare type DocSearchHit = {
|
export type DocSearchHit = {
|
||||||
objectID: string;
|
objectID: string;
|
||||||
content: string | null;
|
content: string | null;
|
||||||
};
|
};
|
||||||
|
|
@ -130,7 +104,7 @@ function createStorage<TItem>(key: string): StorageInterface<TItem> {
|
||||||
- **Utilities**: camelCase (`removeHighlightTags.ts`)
|
- **Utilities**: camelCase (`removeHighlightTags.ts`)
|
||||||
- **Types**: PascalCase (`InternalDocSearchHit`)
|
- **Types**: PascalCase (`InternalDocSearchHit`)
|
||||||
- **Constants**: SCREAMING_SNAKE_CASE (`MAX_QUERY_SIZE`)
|
- **Constants**: SCREAMING_SNAKE_CASE (`MAX_QUERY_SIZE`)
|
||||||
- **CSS classes**: `DocSearch-` prefix with PascalCase (`DocSearch-Modal`)
|
- **CSS classes**: `DocSearch-` prefix
|
||||||
|
|
||||||
### React Components
|
### React Components
|
||||||
|
|
||||||
|
|
@ -140,6 +114,14 @@ function createStorage<TItem>(key: string): StorageInterface<TItem> {
|
||||||
- Use `React.useMemo` for expensive computations
|
- Use `React.useMemo` for expensive computations
|
||||||
- Prefer destructuring props in function signature
|
- Prefer destructuring props in function signature
|
||||||
|
|
||||||
|
### React Component Structure
|
||||||
|
|
||||||
|
- `src/components/ui/` contains reusable rendering components.
|
||||||
|
- Prefer domain-light primitives in `src/components/ui/` when possible.
|
||||||
|
- Feature-scoped UI components may live in `src/components/ui/` when their feature scope is explicit in the filename, such as `RecentConversationsResults.tsx`.
|
||||||
|
- `src/components/` contains scoped composition components that own feature flow, branching, and state orchestration.
|
||||||
|
- Keep AI-specific behavior out of generic UI primitives. If a UI component is AI-specific, make that scope clear in its name.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function DocSearchComponent(
|
function DocSearchComponent(
|
||||||
props: DocSearchProps,
|
props: DocSearchProps,
|
||||||
|
|
@ -169,7 +151,7 @@ try {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Formatting (Prettier)
|
### Formatting (oxfmt)
|
||||||
|
|
||||||
- Single quotes for strings
|
- Single quotes for strings
|
||||||
- Trailing commas (ES5 style)
|
- Trailing commas (ES5 style)
|
||||||
|
|
@ -226,6 +208,8 @@ describe('ComponentName', () => {
|
||||||
packages/
|
packages/
|
||||||
docsearch-react/
|
docsearch-react/
|
||||||
src/
|
src/
|
||||||
|
components/ # Scoped React composition components
|
||||||
|
ui/ # Reusable rendering components and explicitly scoped UI pieces
|
||||||
__tests__/ # Test files
|
__tests__/ # Test files
|
||||||
icons/ # Icon components
|
icons/ # Icon components
|
||||||
types/ # Type definitions
|
types/ # Type definitions
|
||||||
|
|
@ -243,3 +227,17 @@ packages/
|
||||||
- `marked` - Markdown rendering
|
- `marked` - Markdown rendering
|
||||||
- `rollup` - Build bundling
|
- `rollup` - Build bundling
|
||||||
- `vitest` - Test runner
|
- `vitest` - Test runner
|
||||||
|
|
||||||
|
## Cursor Cloud specific instructions
|
||||||
|
|
||||||
|
Toolchain is pinned in `.tool-versions`: Node `24.13.1` (managed via `fnm`) and Bun `1.3.10`. These are preinstalled in the Cloud VM and available on `PATH` in new shells; the startup update script only runs `bun install`.
|
||||||
|
|
||||||
|
Non-obvious caveats:
|
||||||
|
|
||||||
|
- **To run/demo the widget, use the React playground:** `bun run playground:start` serves at `http://localhost:5173` (Vite). `bun run playground-js:start` serves the vanilla-JS demo. These connect to Algolia's hosted index using public credentials baked into the demo, so **outbound internet is required** for live search results.
|
||||||
|
- Run unit tests non-interactively with `bun run test --run` (plain `bun run test` starts Vitest watch mode).
|
||||||
|
- `bun run lint:css` reports many pre-existing CSS lint violations in the repo; these are not environment problems.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- When writing or working on the documentation website (`packages/website`), MUST adhere to the writing guidelines in @packages/website/WRITING_GUIDE.md
|
||||||
|
|
|
||||||
52
CHANGELOG.md
52
CHANGELOG.md
|
|
@ -1,55 +1,3 @@
|
||||||
# [4.7.0](https://github.com/algolia/docsearch/compare/v4.6.3...v4.7.0) (2026-07-27)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **agentStudio:** thread depth error message ([#2881](https://github.com/algolia/docsearch/issues/2881)) ([f68e522](https://github.com/algolia/docsearch/commit/f68e52251ce464e199f12b7a1ac907de61982993))
|
|
||||||
* **askai:** copy the full answer instead of the first text part ([#2910](https://github.com/algolia/docsearch/issues/2910)) ([96476dd](https://github.com/algolia/docsearch/commit/96476dd6f152b0aea134112f5e25afe6b1cc84f8))
|
|
||||||
* **core:** prevent search button from intercepting browser shortcuts when focused ([#2871](https://github.com/algolia/docsearch/issues/2871)) ([0e41a78](https://github.com/algolia/docsearch/commit/0e41a78c44e9f731d825a75957e5668aeae30d69))
|
|
||||||
* **docs:** fix spelling, grammar, and formatting across docs ([#2882](https://github.com/algolia/docsearch/issues/2882)) ([047a769](https://github.com/algolia/docsearch/commit/047a769791fddc31d4d6d3c535f41901a7fcd4c5))
|
|
||||||
* increase line height to avoid overlap ([#2908](https://github.com/algolia/docsearch/issues/2908)) ([2f63848](https://github.com/algolia/docsearch/commit/2f638487804d9a1e753231dba4c02634c79be217))
|
|
||||||
* **modal:** stop over-truncating snippets on mobile and wrap long hits ([#2907](https://github.com/algolia/docsearch/issues/2907)) ([9ad6d16](https://github.com/algolia/docsearch/commit/9ad6d169fee62cb9558e40aa8475f99ecd41e7fe))
|
|
||||||
* sanitize Ask AI markdown HTML to prevent XSS ([#2929](https://github.com/algolia/docsearch/issues/2929)) ([681cbfe](https://github.com/algolia/docsearch/commit/681cbfec03d742cd61b9dfa47b9dbd113f6bce8e))
|
|
||||||
* **sidepanel-js:** expose Agent Studio types in SidepanelProps ([#2906](https://github.com/algolia/docsearch/issues/2906)) ([4710d0c](https://github.com/algolia/docsearch/commit/4710d0ca77e0f12adc720a7a94b0c65a1c01b029))
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **askai:** Ask AI deprecation warning ([#2885](https://github.com/algolia/docsearch/issues/2885)) ([60a2c7b](https://github.com/algolia/docsearch/commit/60a2c7b918a39dfa31f8a540408fc4113420e5dd))
|
|
||||||
* **chore:** one trust cookie consent ([#2887](https://github.com/algolia/docsearch/issues/2887)) ([17751d3](https://github.com/algolia/docsearch/commit/17751d3bc95cf92d0feab5c7781c2ae311c67bfc))
|
|
||||||
* show page path breadcrumbs in Docusaurus search ([#2790](https://github.com/algolia/docsearch/issues/2790)) ([d3d707d](https://github.com/algolia/docsearch/commit/d3d707d8752e054daff1c1e9c559a582ce4d8ee5))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [4.6.3](https://github.com/algolia/docsearch/compare/v4.6.2...v4.6.3) (2026-04-27)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **agentStudio:** cost controls errors surface ([#2878](https://github.com/algolia/docsearch/issues/2878)) ([166b76d](https://github.com/algolia/docsearch/commit/166b76de2f7e728c7dd9994257fb7c6d2497a9c6))
|
|
||||||
* **agentStudio:** surface thread depth error ([#2877](https://github.com/algolia/docsearch/issues/2877)) ([cf42ab5](https://github.com/algolia/docsearch/commit/cf42ab519b7f4b80a1ae7c8425846bceab1b7bd2))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [4.6.2](https://github.com/algolia/docsearch/compare/v4.6.1...v4.6.2) (2026-03-26)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* rollup version ([eac7019](https://github.com/algolia/docsearch/commit/eac70196eb38794aff8c8e1256a77ad8086c4288))
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* agent studio feedback integration ([#2868](https://github.com/algolia/docsearch/issues/2868)) ([f83f5a9](https://github.com/algolia/docsearch/commit/f83f5a911c84ec86dee3db167567679a092abd32))
|
|
||||||
* **askai:** add compatibility with algolia mcp search tool [DASH-2294] ([#2862](https://github.com/algolia/docsearch/issues/2862)) ([e20d30d](https://github.com/algolia/docsearch/commit/e20d30de7b2cbd297529e9b0449b1d8147858a9b))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [4.6.1](https://github.com/algolia/docsearch/compare/v4.6.0...v4.6.1) (2026-03-24)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [4.6.0](https://github.com/algolia/docsearch/compare/v4.5.4...v4.6.0) (2026-02-17)
|
# [4.6.0](https://github.com/algolia/docsearch/compare/v4.5.4...v4.6.0) (2026-02-17)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# This file list code owners for the repository, or part of it.
|
# This file list code owners for the repository, or part of it.
|
||||||
# See https://help.github.com/articles/about-code-owners/
|
# See https://help.github.com/articles/about-code-owners/
|
||||||
|
|
||||||
* @shortcuts @8bittitan @dylantientcheu @NatanTechofNY @vascobettencourt @pipeline1987
|
* @shortcuts @8bittitan @dylantientcheu @NatanTechofNY @LorrisSaintGenez @pipeline1987
|
||||||
|
|
|
||||||
|
|
@ -60,12 +60,43 @@ Some examples of valid commit messages (used as first lines):
|
||||||
To run this project, you will need:
|
To run this project, you will need:
|
||||||
|
|
||||||
- Node.js ≥ 18 – [nvm](https://github.com/nvm-sh/nvm#install-script) is recommended
|
- Node.js ≥ 18 – [nvm](https://github.com/nvm-sh/nvm#install-script) is recommended
|
||||||
- [Yarn](https://yarnpkg.com)
|
- [Bun](https://bun.sh) – our package manager and build tool
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Install dependencies
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# Build all packages
|
||||||
|
bun run build
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
bun run test
|
||||||
|
|
||||||
|
# Start development mode (watch + playground)
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Changesets
|
||||||
|
|
||||||
|
This project uses [Changesets](https://github.com/changesets/changesets) for versioning and publishing.
|
||||||
|
|
||||||
|
If your PR includes changes that should be released, add a changeset:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun run changeset
|
||||||
|
```
|
||||||
|
|
||||||
|
Select the packages affected by your change, choose the version bump type (major/minor/patch), and write a summary that will appear in the changelog.
|
||||||
|
|
||||||
## Release
|
## Release
|
||||||
|
|
||||||
```sh
|
When changesets are merged to `main`, a "Version Packages" PR will be automatically created (or updated). Merging that PR triggers the release workflow:
|
||||||
yarn run release
|
|
||||||
```
|
|
||||||
|
|
||||||
It will create a pull request for the next release. When it's reviewed, approved and merged, then CircleCI will automatically publish it to npm.
|
1. Package versions are bumped
|
||||||
|
2. CHANGELOGs are updated
|
||||||
|
3. Packages are published to npm
|
||||||
|
4. Git tags are created
|
||||||
|
|
||||||
|
All `@docsearch/*` packages share the same version number (fixed versioning).
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -33,7 +33,7 @@ DocSearch crawls your documentation, pushes the content to an Algolia index and
|
||||||
#### Installation
|
#### Installation
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yarn add @docsearch/js@4
|
bun add @docsearch/js@4
|
||||||
# or
|
# or
|
||||||
npm install @docsearch/js@4
|
npm install @docsearch/js@4
|
||||||
```
|
```
|
||||||
|
|
@ -64,7 +64,7 @@ import '@docsearch/css';
|
||||||
docsearch({
|
docsearch({
|
||||||
container: '#docsearch',
|
container: '#docsearch',
|
||||||
appId: 'YOUR_APP_ID',
|
appId: 'YOUR_APP_ID',
|
||||||
indexName: 'YOUR_INDEX_NAME',
|
indices: ['YOUR_INDEX_NAME'],
|
||||||
apiKey: 'YOUR_SEARCH_API_KEY',
|
apiKey: 'YOUR_SEARCH_API_KEY',
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -74,7 +74,7 @@ docsearch({
|
||||||
#### Installation
|
#### Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn add @docsearch/react@4
|
bun add @docsearch/react@4
|
||||||
# or
|
# or
|
||||||
npm install @docsearch/react@4
|
npm install @docsearch/react@4
|
||||||
```
|
```
|
||||||
|
|
@ -111,6 +111,10 @@ export default App;
|
||||||
|
|
||||||
[Read documentation →](https://docsearch.algolia.com/docs/styling)
|
[Read documentation →](https://docsearch.algolia.com/docs/styling)
|
||||||
|
|
||||||
|
## MCP plugins
|
||||||
|
|
||||||
|
Client plugins for the public DocSearch MCP endpoint are in [`mcp/plugins/docsearch`](mcp/plugins/docsearch). They connect ChatGPT, Codex, Cursor, and Claude Code to `https://mcp.algolia.com/1/docsearch/mcp` for current public developer documentation.
|
||||||
|
|
||||||
## Related projects
|
## Related projects
|
||||||
|
|
||||||
DocSearch is made of the following repositories:
|
DocSearch is made of the following repositories:
|
||||||
|
|
|
||||||
146
adapters/docusaurus-theme-search-algolia/CHANGELOG.md
Normal file
146
adapters/docusaurus-theme-search-algolia/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
# @docsearch/docusaurus-adapter
|
||||||
|
|
||||||
|
## 5.0.0-beta.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [ee9fddb]
|
||||||
|
- @docsearch/react@5.0.0-beta.4
|
||||||
|
- @docsearch/sidepanel@5.0.0-beta.4
|
||||||
|
- @docsearch/modal@5.0.0-beta.4
|
||||||
|
- @docsearch/core@5.0.0-beta.4
|
||||||
|
|
||||||
|
## 5.0.0-beta.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- fe4d0c2: Align Ask AI dynamic indices with Agent Studio completions: `askAi.indices` is now `string[]` (index names only). Use `askAi.searchParameters` for per-index runtime overrides.
|
||||||
|
- c6a1ce1: fix(docusaurus-adapter): Docusaurus styling cleanup
|
||||||
|
- Updated dependencies [3f74c33]
|
||||||
|
- Updated dependencies [b33a7f7]
|
||||||
|
- Updated dependencies [fe4d0c2]
|
||||||
|
- Updated dependencies [4e44b55]
|
||||||
|
- Updated dependencies [9a1b3e4]
|
||||||
|
- Updated dependencies [b28bc84]
|
||||||
|
- @docsearch/core@5.0.0-beta.3
|
||||||
|
- @docsearch/react@5.0.0-beta.3
|
||||||
|
- @docsearch/modal@5.0.0-beta.3
|
||||||
|
- @docsearch/sidepanel@5.0.0-beta.3
|
||||||
|
|
||||||
|
## 5.0.0-beta.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 5eac1fd: feat(v5): General UI styling updates and fixes
|
||||||
|
|
||||||
|
- New `--docsearch-font-family` variable, used by the search button, keyboard
|
||||||
|
keys, modal, and sidepanel. It replaces the system font stacks that were
|
||||||
|
duplicated across `sidepanel.css` and `button.css`, so overriding one
|
||||||
|
variable now themes every DocSearch surface
|
||||||
|
- The search input and modal heading are `1rem` at every breakpoint
|
||||||
|
(previously `0.875rem` with a mobile-only override)
|
||||||
|
- `.DocSearch-Title` uses `line-height: 1.5em` instead of `0.5em` and adds
|
||||||
|
`overflow-wrap: anywhere`, so long titles wrap instead of overlapping (#2908)
|
||||||
|
- Hit icon `svg` sizing moved into `.DocSearch-Hit-icon`. The
|
||||||
|
`.DocSearch-Hit-icon--small` modifier is replaced by
|
||||||
|
`.DocSearch-Hit-icon--start`, which top-aligns the icon; recent
|
||||||
|
conversations use it
|
||||||
|
- The Ask AI button icon centers with `display: inline-flex` instead of
|
||||||
|
`margin-block-start`/`align-self` overrides
|
||||||
|
- Removed the 2px offset on the Ask AI sources action text
|
||||||
|
- `SourcesPanel` accepts `pluralTitleText`. `titleText` is now the singular
|
||||||
|
label, and the trigger renders `{count} {label}`
|
||||||
|
- `AskAiScreenTranslations` and `ConversationScreenTranslations` expose
|
||||||
|
`relatedSourcesTextPlural`
|
||||||
|
- Docusaurus adapter: `theme.SearchModal.askAiScreen.relatedSourcesText` is
|
||||||
|
now the singular "Source", and the new
|
||||||
|
`theme.SearchModal.askAiScreen.relatedSourcesTextPlural` provides "Sources"
|
||||||
|
|
||||||
|
- 4f6b5b1: feat(v5): UI and DX updates
|
||||||
|
|
||||||
|
- Rename the Ask AI assistantId option to agentId (adapter theme.SearchModal.askAi.assistantId → agentId)
|
||||||
|
- appId and apiKey moved up into @docsearch/core, so they're configured once and shared
|
||||||
|
- Removed the indexName prop from the Sidepanel
|
||||||
|
- Facet defaults can now be read from the index searchParameters
|
||||||
|
- Restored nested grouping of search results
|
||||||
|
|
||||||
|
- Updated dependencies [5eac1fd]
|
||||||
|
- Updated dependencies [4f6b5b1]
|
||||||
|
- Updated dependencies [9fe6738]
|
||||||
|
- @docsearch/react@5.0.0-beta.2
|
||||||
|
- @docsearch/sidepanel@5.0.0-beta.2
|
||||||
|
- @docsearch/modal@5.0.0-beta.2
|
||||||
|
- @docsearch/core@5.0.0-beta.2
|
||||||
|
|
||||||
|
## 5.0.0-beta.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 09861c8: Align CSS class and animation names with the `DocSearch-` naming convention.
|
||||||
|
|
||||||
|
- The `shimmer` utility class is now `DocSearch-shimmer`
|
||||||
|
- Fixed a typo in `DocSearck-AskAiScreen-MessageContent-Stopped`, now
|
||||||
|
`DocSearch-AskAiScreen-MessageContent-Stopped`
|
||||||
|
- Keyframes renamed to kebab-case: `shimmerText` → `shimmer-text`,
|
||||||
|
`slideDown` → `slide-down`, `fadeIn` → `fade-in`
|
||||||
|
- Dark theme selectors use `:root[data-theme='dark']` instead of
|
||||||
|
`html[data-theme='dark']`
|
||||||
|
- Dropped redundant type qualifiers from `.DocSearch-Hits-padded` and
|
||||||
|
`.DocSearch-Hit-Select-Icon` selectors, slightly lowering their specificity
|
||||||
|
|
||||||
|
If you override these classes or keyframes in custom styles, update your
|
||||||
|
selectors accordingly.
|
||||||
|
|
||||||
|
- Updated dependencies [f8e0678]
|
||||||
|
- Updated dependencies [09861c8]
|
||||||
|
- @docsearch/react@5.0.0-beta.1
|
||||||
|
- @docsearch/modal@5.0.0-beta.1
|
||||||
|
- @docsearch/sidepanel@5.0.0-beta.1
|
||||||
|
- @docsearch/core@5.0.0-beta.1
|
||||||
|
|
||||||
|
## 5.0.0-beta.0
|
||||||
|
|
||||||
|
### Major Changes
|
||||||
|
|
||||||
|
- a8ed1ea: # DocSearch v5-beta
|
||||||
|
|
||||||
|
DocSearch v5 is a major release that introduces AI-powered answers via Agent
|
||||||
|
Studio, a refreshed search UI, and a modernized package architecture.
|
||||||
|
|
||||||
|
## Ask AI & Agent Studio
|
||||||
|
|
||||||
|
- Agent Studio integration with core tools and dynamic tool calls
|
||||||
|
- Conversation memory support
|
||||||
|
- Compatibility with the Algolia MCP search tool, with aggregated MCP search
|
||||||
|
tool calls
|
||||||
|
- Dynamic index selection for Agent Studio
|
||||||
|
- Feedback integration, including feedback notes and tags
|
||||||
|
- Prompt suggestions in keyword search and follow-up prompt suggestions
|
||||||
|
- Ask AI modal split into its own component and Ask AI transport layer removed
|
||||||
|
|
||||||
|
## Search UI
|
||||||
|
|
||||||
|
- Refreshed v5 UI with improved dark theme, sources panel, and accessibility
|
||||||
|
- Faceted search with filter chips
|
||||||
|
- Hit breadcrumbs and result badges
|
||||||
|
|
||||||
|
## Packaging & architecture
|
||||||
|
|
||||||
|
- New `@docsearch/cli` package for MCP setup and search
|
||||||
|
- MCP plugin support
|
||||||
|
- Split JS bundles for search-only usage and JS-based hybrid mode
|
||||||
|
- Migrated the build system to tsdown
|
||||||
|
- Migrated CSS building to LightningCSS
|
||||||
|
|
||||||
|
## Breaking changes
|
||||||
|
|
||||||
|
- Ask AI related props are now nested under a single root `askai` option
|
||||||
|
- The Ask AI transport layer has been removed
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8ed1ea]
|
||||||
|
- @docsearch/core@5.0.0-beta.0
|
||||||
|
- @docsearch/modal@5.0.0-beta.0
|
||||||
|
- @docsearch/react@5.0.0-beta.0
|
||||||
|
- @docsearch/sidepanel@5.0.0-beta.0
|
||||||
|
|
@ -14,14 +14,14 @@ export default {
|
||||||
docsearch: {
|
docsearch: {
|
||||||
appId: 'APP_ID',
|
appId: 'APP_ID',
|
||||||
apiKey: 'SEARCH_API_KEY',
|
apiKey: 'SEARCH_API_KEY',
|
||||||
indexName: 'INDEX_NAME',
|
indices: [{ name: 'INDEX_NAME' }],
|
||||||
askAi: {
|
askAi: {
|
||||||
assistantId: 'ASSISTANT_ID',
|
assistantId: 'ASSISTANT_ID',
|
||||||
sidePanel: true,
|
|
||||||
},
|
},
|
||||||
|
sidePanel: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
`themeConfig.algolia` is still supported as a backward-compatible alias.
|
Only `themeConfig.docsearch` is supported.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@docsearch/docusaurus-adapter",
|
"name": "@docsearch/docusaurus-adapter",
|
||||||
"version": "4.7.0",
|
"version": "5.0.0-beta.4",
|
||||||
"description": "Algolia search component for Docusaurus.",
|
"description": "Algolia search component for Docusaurus.",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"sideEffects": [
|
"sideEffects": [
|
||||||
|
|
@ -27,35 +27,39 @@
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yarn exec tsc --build --force && node ./scripts/copy-assets.mjs && node ./scripts/format-theme.mjs",
|
"build": "bun x tsc --build --force && node ./scripts/copy-assets.mjs && oxfmt --config ../../.oxfmtrc.json ./lib/theme/**/*",
|
||||||
"build:clean": "yarn clean && yarn build",
|
"build:clean": "bun run clean && bun run build",
|
||||||
"clean": "rm -rf lib tsconfig.*.tsbuildinfo",
|
"clean": "rm -rf lib tsconfig.*.tsbuildinfo",
|
||||||
"watch": "run-p -c copy:watch build:watch",
|
"watch": "run-p -c copy:watch build:watch",
|
||||||
"build:watch": "yarn exec tsc --build --watch",
|
"build:watch": "bun x tsc --build --watch",
|
||||||
"copy:watch": "node ./scripts/copy-assets.mjs --watch"
|
"copy:watch": "node ./scripts/copy-assets.mjs --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docsearch/react": "4.7.0",
|
"@algolia/autocomplete-core": "1.19.2",
|
||||||
"@docusaurus/core": "^3.9.2",
|
"@docsearch/core": "workspace:*",
|
||||||
"@docusaurus/plugin-content-docs": "^3.9.2",
|
"@docsearch/modal": "workspace:*",
|
||||||
"@docusaurus/theme-common": "^3.9.2",
|
"@docsearch/react": "workspace:*",
|
||||||
"@docusaurus/theme-translations": "^3.9.2",
|
"@docsearch/sidepanel": "workspace:*",
|
||||||
|
"@docusaurus/core": "^3.10.2",
|
||||||
|
"@docusaurus/plugin-content-docs": "^3.10.2",
|
||||||
|
"@docusaurus/theme-common": "^3.10.2",
|
||||||
|
"@docusaurus/theme-translations": "^3.10.2",
|
||||||
"algoliasearch": "^5.37.0",
|
"algoliasearch": "^5.37.0",
|
||||||
"algoliasearch-helper": "^3.26.0",
|
"algoliasearch-helper": "^3.26.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"eta": "^2.2.0",
|
"eta": "^2.2.0",
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^11.1.1",
|
||||||
"joi": "^17.9.2",
|
"joi": "^17.9.2",
|
||||||
"lodash": "^4.17.21",
|
"search-insights": "^2.17.3",
|
||||||
"tslib": "^2.6.0",
|
"tslib": "^2.6.0"
|
||||||
"utility-types": "^3.10.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/core": "^3.9.2",
|
"@docusaurus/core": "^3.10.2",
|
||||||
"@docusaurus/module-type-aliases": "3.9.2",
|
"@docusaurus/module-type-aliases": "3.10.2",
|
||||||
"@docusaurus/theme-classic": "3.9.2",
|
"@docusaurus/theme-classic": "3.10.2",
|
||||||
|
"@docusaurus/types": "^3.10.2",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/lodash": "^4.17.10",
|
"glob": "^13.0.6",
|
||||||
"typescript": "5.7.3"
|
"typescript": "5.7.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ async function copyAssetsOnce() {
|
||||||
}
|
}
|
||||||
return ASSET_EXTENSIONS.has(extension);
|
return ASSET_EXTENSIONS.has(extension);
|
||||||
})
|
})
|
||||||
.map((filePath) => copyAssetFile(filePath)),
|
.map((filePath) => copyAssetFile(filePath))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { execSync } from 'node:child_process';
|
|
||||||
|
|
||||||
import { glob } from 'glob';
|
|
||||||
|
|
||||||
const pattern = 'lib/theme/**/*.js';
|
|
||||||
const files = glob.sync(pattern);
|
|
||||||
|
|
||||||
if (files.length > 0) {
|
|
||||||
try {
|
|
||||||
execSync(`prettier --config ../../.prettierrc --write "${pattern}"`, {
|
|
||||||
stdio: 'inherit',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Prettier failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
|
|
@ -17,7 +17,11 @@ describe('mergeFacetFilters', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('merges [string,array]', () => {
|
it('merges [string,array]', () => {
|
||||||
assert.deepStrictEqual(mergeFacetFilters('f1', ['f2', 'f3']), ['f1', 'f2', 'f3']);
|
assert.deepStrictEqual(mergeFacetFilters('f1', ['f2', 'f3']), [
|
||||||
|
'f1',
|
||||||
|
'f2',
|
||||||
|
'f3',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('merges [string,undefined]', () => {
|
it('merges [string,undefined]', () => {
|
||||||
|
|
@ -29,16 +33,27 @@ describe('mergeFacetFilters', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('merges [array,undefined]', () => {
|
it('merges [array,undefined]', () => {
|
||||||
assert.deepStrictEqual(mergeFacetFilters(['f1', 'f2'], undefined), ['f1', 'f2']);
|
assert.deepStrictEqual(mergeFacetFilters(['f1', 'f2'], undefined), [
|
||||||
|
'f1',
|
||||||
|
'f2',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('merges [undefined,array]', () => {
|
it('merges [undefined,array]', () => {
|
||||||
assert.deepStrictEqual(mergeFacetFilters(undefined, ['f1', 'f2']), ['f1', 'f2']);
|
assert.deepStrictEqual(mergeFacetFilters(undefined, ['f1', 'f2']), [
|
||||||
|
'f1',
|
||||||
|
'f2',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('merges [array,array]', () => {
|
it('merges [array,array]', () => {
|
||||||
assert.deepStrictEqual(mergeFacetFilters(['f1'], ['f2']), ['f1', 'f2']);
|
assert.deepStrictEqual(mergeFacetFilters(['f1'], ['f2']), ['f1', 'f2']);
|
||||||
|
|
||||||
assert.deepStrictEqual(mergeFacetFilters(['f1', 'f2'], ['f3', 'f4']), ['f1', 'f2', 'f3', 'f4']);
|
assert.deepStrictEqual(mergeFacetFilters(['f1', 'f2'], ['f3', 'f4']), [
|
||||||
|
'f1',
|
||||||
|
'f2',
|
||||||
|
'f3',
|
||||||
|
'f4',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { useAlgoliaThemeConfig } from './useAlgoliaThemeConfig';
|
export { useAlgoliaThemeConfig } from './useAlgoliaThemeConfig';
|
||||||
|
|
|
||||||
|
|
@ -1,121 +1,112 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { AskAiConfig } from '@docsearch/docusaurus-adapter';
|
import type { AskAiConfig } from '@docsearch/docusaurus-adapter';
|
||||||
import type { DocSearchModalProps, DocSearchTranslations } from '@docsearch/react';
|
import type { DocSearchAskAi, DocSearchProps } from '@docsearch/react';
|
||||||
import translations from '@theme/SearchTranslations';
|
|
||||||
import type { FacetFilters } from 'algoliasearch/lite';
|
import type { FacetFilters } from 'algoliasearch/lite';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { useAlgoliaContextualFacetFiltersIfEnabled } from './useAlgoliaContextualFacetFilters';
|
import { useAlgoliaContextualFacetFiltersIfEnabled } from './useAlgoliaContextualFacetFilters';
|
||||||
import { mergeFacetFilters } from './utils';
|
|
||||||
|
|
||||||
|
type AskAiOptions = AskAiConfig & Pick<DocSearchAskAi, 'tools'>;
|
||||||
// The minimal props the hook needs from DocSearch
|
// The minimal props the hook needs from DocSearch
|
||||||
interface DocSearchPropsLite {
|
interface DocSearchPropsLite {
|
||||||
indexName: string;
|
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
appId: string;
|
appId: string;
|
||||||
placeholder?: string;
|
indices: NonNullable<DocSearchProps['indices']>;
|
||||||
translations?: DocSearchTranslations;
|
askAi?: AskAiOptions;
|
||||||
searchParameters?: DocSearchModalProps['searchParameters'];
|
|
||||||
askAi?: AskAiConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OnAskAiToggle = NonNullable<DocSearchModalProps['onAskAiToggle']>;
|
|
||||||
type AskAiConfigWithoutSidePanel = Omit<AskAiConfig, 'sidePanel'>;
|
|
||||||
type DocSearchAskAi = Exclude<DocSearchModalProps['askAi'], string | undefined>;
|
|
||||||
type DocSearchModalPropsLite = Partial<Omit<DocSearchModalProps, 'askAi'>>;
|
|
||||||
|
|
||||||
type UseAskAiResult = {
|
type UseAskAiResult = {
|
||||||
canHandleAskAi: boolean;
|
modalAskAi?: DocSearchAskAi;
|
||||||
isAskAiActive: boolean;
|
sidePanelAskAi?: DocSearchAskAi & {
|
||||||
currentPlaceholder: string | undefined;
|
apiKey: string;
|
||||||
onAskAiToggle: OnAskAiToggle;
|
appId: string;
|
||||||
askAi?: AskAiConfig;
|
indexName: string;
|
||||||
extraAskAiProps: DocSearchModalPropsLite & {
|
|
||||||
askAi?: DocSearchAskAi;
|
|
||||||
canHandleAskAi?: boolean;
|
|
||||||
isAskAiActive?: boolean;
|
|
||||||
onAskAiToggle?: OnAskAiToggle;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getIndexName(
|
||||||
|
index: NonNullable<DocSearchProps['indices']>[number]
|
||||||
|
): string {
|
||||||
|
return typeof index === 'string' ? index : index.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAskAiIndexName(
|
||||||
|
askAi: AskAiConfig,
|
||||||
|
indices: NonNullable<DocSearchProps['indices']>
|
||||||
|
): string {
|
||||||
|
return askAi.indices?.[0] ?? getIndexName(indices[0]!);
|
||||||
|
}
|
||||||
|
|
||||||
|
function facetFiltersToFilterString(facetFilters: FacetFilters): string {
|
||||||
|
const items = Array.isArray(facetFilters) ? facetFilters : [facetFilters];
|
||||||
|
|
||||||
|
return items
|
||||||
|
.map((item) =>
|
||||||
|
Array.isArray(item) ? `(${item.join(' OR ')})` : String(item)
|
||||||
|
)
|
||||||
|
.join(' AND ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeFilters(existing: string | undefined, added: string): string {
|
||||||
|
return existing ? `(${existing}) AND (${added})` : added;
|
||||||
|
}
|
||||||
|
|
||||||
// We need to apply contextualSearch facetFilters to AskAI filters
|
// We need to apply contextualSearch facetFilters to AskAI filters
|
||||||
// This can't be done at config normalization time because contextual filters
|
// This can't be done at config normalization time because contextual filters
|
||||||
// can only be determined at runtime
|
// can only be determined at runtime. Agent Studio accepts them via
|
||||||
|
// askAi.searchParameters[index].filters, keyed by dynamic index names.
|
||||||
function applyAskAiContextualSearch(
|
function applyAskAiContextualSearch(
|
||||||
askAi: AskAiConfig | undefined,
|
askAi: AskAiOptions | undefined,
|
||||||
contextualSearchFilters: FacetFilters | undefined,
|
contextualSearchFilters: FacetFilters | undefined
|
||||||
): AskAiConfig | undefined {
|
): AskAiOptions | undefined {
|
||||||
if (!askAi) {
|
if (!askAi || !contextualSearchFilters || !askAi.indices?.length) {
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (askAi.agentStudio === true) {
|
|
||||||
return askAi;
|
return askAi;
|
||||||
}
|
}
|
||||||
if (!contextualSearchFilters) {
|
|
||||||
return askAi;
|
const contextualFilters = facetFiltersToFilterString(contextualSearchFilters);
|
||||||
|
const searchParameters = { ...askAi.searchParameters };
|
||||||
|
|
||||||
|
for (const indexName of askAi.indices) {
|
||||||
|
const current = searchParameters[indexName] ?? {};
|
||||||
|
searchParameters[indexName] = {
|
||||||
|
...current,
|
||||||
|
filters: mergeFilters(current.filters, contextualFilters),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const askAiFacetFilters = askAi.searchParameters?.facetFilters;
|
|
||||||
return {
|
return {
|
||||||
...askAi,
|
...askAi,
|
||||||
searchParameters: {
|
searchParameters,
|
||||||
...askAi.searchParameters,
|
|
||||||
facetFilters: mergeFacetFilters(askAiFacetFilters, contextualSearchFilters),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAlgoliaAskAi(props: DocSearchPropsLite): UseAskAiResult {
|
export function useAlgoliaAskAi(props: DocSearchPropsLite): UseAskAiResult {
|
||||||
const [isAskAiActive, setIsAskAiActive] = useState(false);
|
|
||||||
const contextualSearchFilters = useAlgoliaContextualFacetFiltersIfEnabled();
|
const contextualSearchFilters = useAlgoliaContextualFacetFiltersIfEnabled();
|
||||||
|
|
||||||
const askAi = useMemo(() => {
|
const askAi = useMemo(() => {
|
||||||
return applyAskAiContextualSearch(props.askAi, contextualSearchFilters);
|
return applyAskAiContextualSearch(props.askAi, contextualSearchFilters);
|
||||||
}, [props.askAi, contextualSearchFilters]);
|
}, [props.askAi, contextualSearchFilters]);
|
||||||
|
|
||||||
const askAiWithoutSidePanel = useMemo<AskAiConfigWithoutSidePanel | undefined>(() => {
|
const resolvedAskAi = useMemo<UseAskAiResult['sidePanelAskAi']>(() => {
|
||||||
if (!askAi) {
|
if (!askAi) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const { sidePanel: _sidePanel, ...docsearchAskAi } = askAi;
|
return {
|
||||||
return docsearchAskAi;
|
...askAi,
|
||||||
}, [askAi]);
|
apiKey: props.apiKey,
|
||||||
|
appId: props.appId,
|
||||||
const modalAskAi = useMemo<DocSearchAskAi | undefined>(() => {
|
indexName: getAskAiIndexName(askAi, props.indices),
|
||||||
if (!askAiWithoutSidePanel) {
|
};
|
||||||
return undefined;
|
}, [askAi, props.apiKey, props.appId, props.indices]);
|
||||||
}
|
|
||||||
return askAiWithoutSidePanel as DocSearchAskAi;
|
|
||||||
}, [askAiWithoutSidePanel]);
|
|
||||||
|
|
||||||
const canHandleAskAi = Boolean(askAi);
|
|
||||||
|
|
||||||
const currentPlaceholder = isAskAiActive
|
|
||||||
? translations.modal?.searchBox?.placeholderTextAskAi
|
|
||||||
: translations.modal?.searchBox?.placeholderText || props?.placeholder;
|
|
||||||
|
|
||||||
const onAskAiToggle = useCallback<OnAskAiToggle>((askAiToggle: boolean) => {
|
|
||||||
setIsAskAiActive(askAiToggle);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const extraAskAiProps: UseAskAiResult['extraAskAiProps'] = {
|
|
||||||
askAi: modalAskAi,
|
|
||||||
canHandleAskAi,
|
|
||||||
isAskAiActive,
|
|
||||||
onAskAiToggle,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canHandleAskAi,
|
modalAskAi: resolvedAskAi,
|
||||||
isAskAiActive,
|
sidePanelAskAi: resolvedAskAi,
|
||||||
currentPlaceholder,
|
|
||||||
onAskAiToggle,
|
|
||||||
askAi,
|
|
||||||
extraAskAiProps,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,118 +1,42 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { AskAiConfig } from '@docsearch/docusaurus-adapter';
|
import type { ThemeConfigDocSearch } from '@docsearch/docusaurus-adapter';
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
type AskAiTogglePayload = {
|
type SidepanelOptions = Exclude<
|
||||||
query: string;
|
NonNullable<ThemeConfigDocSearch['sidePanel']>,
|
||||||
messageId?: string;
|
boolean
|
||||||
suggestedQuestionId?: string;
|
>;
|
||||||
};
|
|
||||||
|
|
||||||
type SidepanelOptions = Exclude<NonNullable<AskAiConfig['sidePanel']>, boolean>;
|
|
||||||
|
|
||||||
type UseAlgoliaAskAiSidepanelParams = {
|
type UseAlgoliaAskAiSidepanelParams = {
|
||||||
askAiConfig?: AskAiConfig;
|
sidePanel?: ThemeConfigDocSearch['sidePanel'];
|
||||||
importSidepanel: () => Promise<void>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type UseAlgoliaAskAiSidepanelResult = {
|
type UseAlgoliaAskAiSidepanelResult = {
|
||||||
sidePanelEnabled: boolean;
|
sidePanelEnabled: boolean;
|
||||||
showSidepanelButton: boolean;
|
showSidepanelButton: boolean;
|
||||||
sidePanelOptions?: SidepanelOptions;
|
sidePanelOptions?: SidepanelOptions;
|
||||||
sidePanelAgentStudio: boolean;
|
|
||||||
sidepanelPortalContainer: HTMLElement | null;
|
|
||||||
isSidepanelOpen: boolean;
|
|
||||||
sidepanelInitialMessage?: AskAiTogglePayload;
|
|
||||||
openSidepanel: (payload?: AskAiTogglePayload) => void;
|
|
||||||
closeSidepanel: () => void;
|
|
||||||
toggleSidepanel: () => void;
|
|
||||||
handleSidepanelOpen: () => void;
|
|
||||||
loadSidepanel: () => Promise<void>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useAlgoliaAskAiSidepanel({
|
export function useAlgoliaAskAiSidepanel({
|
||||||
askAiConfig,
|
sidePanel,
|
||||||
importSidepanel,
|
|
||||||
}: UseAlgoliaAskAiSidepanelParams): UseAlgoliaAskAiSidepanelResult {
|
}: UseAlgoliaAskAiSidepanelParams): UseAlgoliaAskAiSidepanelResult {
|
||||||
const [isSidepanelOpen, setIsSidepanelOpen] = useState(false);
|
const sidePanelOptions = useMemo(
|
||||||
const [sidepanelInitialMessage, setSidepanelInitialMessage] = useState<AskAiTogglePayload | undefined>(undefined);
|
() => (typeof sidePanel === 'object' ? sidePanel : undefined),
|
||||||
const openRequestId = useRef(0);
|
[sidePanel]
|
||||||
|
|
||||||
const sidePanelConfig = askAiConfig?.sidePanel;
|
|
||||||
const sidePanelEnabled = Boolean(sidePanelConfig);
|
|
||||||
const sidePanelOptions = typeof sidePanelConfig === 'object' ? sidePanelConfig : undefined;
|
|
||||||
const showSidepanelButton = sidePanelEnabled && sidePanelOptions?.hideButton !== true;
|
|
||||||
const sidePanelAgentStudio = askAiConfig?.agentStudio ?? false;
|
|
||||||
|
|
||||||
const sidepanelPortalContainer = useMemo(() => {
|
|
||||||
return typeof document !== 'undefined' ? document.body : null;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadSidepanel = useCallback(() => {
|
|
||||||
return importSidepanel();
|
|
||||||
}, [importSidepanel]);
|
|
||||||
|
|
||||||
const openSidepanel = useCallback(
|
|
||||||
(payload?: AskAiTogglePayload) => {
|
|
||||||
if (!sidePanelEnabled || !askAiConfig) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const initialMessage =
|
|
||||||
payload?.query && payload.query.length > 0
|
|
||||||
? {
|
|
||||||
query: payload.query,
|
|
||||||
messageId: payload.messageId,
|
|
||||||
suggestedQuestionId: payload.suggestedQuestionId,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
const requestId = openRequestId.current + 1;
|
|
||||||
openRequestId.current = requestId;
|
|
||||||
setSidepanelInitialMessage(initialMessage);
|
|
||||||
loadSidepanel().then(() => {
|
|
||||||
if (openRequestId.current === requestId) {
|
|
||||||
setIsSidepanelOpen(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[askAiConfig, loadSidepanel, sidePanelEnabled],
|
|
||||||
);
|
);
|
||||||
|
const sidePanelEnabled = Boolean(sidePanel);
|
||||||
const closeSidepanel = useCallback(() => {
|
const showSidepanelButton =
|
||||||
openRequestId.current += 1;
|
sidePanelEnabled && sidePanelOptions?.hideButton !== true;
|
||||||
setIsSidepanelOpen(false);
|
|
||||||
setSidepanelInitialMessage(undefined);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleSidepanel = useCallback(() => {
|
|
||||||
if (isSidepanelOpen) {
|
|
||||||
closeSidepanel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
openSidepanel();
|
|
||||||
}, [closeSidepanel, isSidepanelOpen, openSidepanel]);
|
|
||||||
|
|
||||||
const handleSidepanelOpen = useCallback(() => {
|
|
||||||
setIsSidepanelOpen(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sidePanelEnabled,
|
sidePanelEnabled,
|
||||||
showSidepanelButton,
|
showSidepanelButton,
|
||||||
sidePanelOptions,
|
sidePanelOptions,
|
||||||
sidePanelAgentStudio,
|
|
||||||
sidepanelPortalContainer,
|
|
||||||
isSidepanelOpen,
|
|
||||||
sidepanelInitialMessage,
|
|
||||||
openSidepanel,
|
|
||||||
closeSidepanel,
|
|
||||||
toggleSidepanel,
|
|
||||||
handleSidepanelOpen,
|
|
||||||
loadSidepanel,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useDocsContextualSearchTags } from '@docusaurus/plugin-content-docs/client';
|
import { useDocsContextualSearchTags } from '@docusaurus/plugin-content-docs/client';
|
||||||
|
|
@ -45,7 +45,9 @@ export function useAlgoliaContextualFacetFilters(): FacetFilters {
|
||||||
return [languageFilter, tagsFilter];
|
return [languageFilter, tagsFilter];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAlgoliaContextualFacetFiltersIfEnabled(): FacetFilters | undefined {
|
export function useAlgoliaContextualFacetFiltersIfEnabled():
|
||||||
|
| FacetFilters
|
||||||
|
| undefined {
|
||||||
const { contextualSearch } = useAlgoliaThemeConfig();
|
const { contextualSearch } = useAlgoliaThemeConfig();
|
||||||
const facetFilters = useAlgoliaContextualFacetFilters();
|
const facetFilters = useAlgoliaContextualFacetFilters();
|
||||||
if (contextualSearch) {
|
if (contextualSearch) {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,18 @@
|
||||||
|
import type {
|
||||||
|
ThemeConfig,
|
||||||
|
ThemeConfigDocSearch,
|
||||||
|
} from '@docsearch/docusaurus-adapter';
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
import type { ThemeConfig, ThemeConfigAlgolia } from '@docsearch/docusaurus-adapter';
|
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
|
|
||||||
import { getDocSearchConfig } from '../getDocSearchConfig';
|
import { getDocSearchConfig } from '../getDocSearchConfig';
|
||||||
|
|
||||||
export function useAlgoliaThemeConfig(): ThemeConfigAlgolia {
|
export function useAlgoliaThemeConfig(): ThemeConfigDocSearch {
|
||||||
const {
|
const {
|
||||||
siteConfig: { themeConfig },
|
siteConfig: { themeConfig },
|
||||||
} = useDocusaurusContext();
|
} = useDocusaurusContext();
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ThemeConfigAlgolia } from '@docsearch/docusaurus-adapter';
|
import type { ThemeConfigDocSearch } from '@docsearch/docusaurus-adapter';
|
||||||
import { isRegexpStringMatch } from '@docusaurus/theme-common';
|
import { isRegexpStringMatch } from '@docusaurus/theme-common';
|
||||||
import { useBaseUrlUtils } from '@docusaurus/useBaseUrl';
|
import { useBaseUrlUtils } from '@docusaurus/useBaseUrl';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
@ -14,10 +14,13 @@ import { useAlgoliaThemeConfig } from './useAlgoliaThemeConfig';
|
||||||
|
|
||||||
function replacePathname(
|
function replacePathname(
|
||||||
pathname: string,
|
pathname: string,
|
||||||
replaceSearchResultPathname: ThemeConfigAlgolia['replaceSearchResultPathname'],
|
replaceSearchResultPathname: ThemeConfigDocSearch['replaceSearchResultPathname']
|
||||||
): string {
|
): string {
|
||||||
return replaceSearchResultPathname
|
return replaceSearchResultPathname
|
||||||
? pathname.replaceAll(new RegExp(replaceSearchResultPathname.from, 'g'), replaceSearchResultPathname.to)
|
? pathname.replaceAll(
|
||||||
|
new RegExp(replaceSearchResultPathname.from, 'g'),
|
||||||
|
replaceSearchResultPathname.to
|
||||||
|
)
|
||||||
: pathname;
|
: pathname;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,7 +30,8 @@ function replacePathname(
|
||||||
*/
|
*/
|
||||||
export function useSearchResultUrlProcessor(): (url: string) => string {
|
export function useSearchResultUrlProcessor(): (url: string) => string {
|
||||||
const { withBaseUrl } = useBaseUrlUtils();
|
const { withBaseUrl } = useBaseUrlUtils();
|
||||||
const { externalUrlRegex, replaceSearchResultPathname } = useAlgoliaThemeConfig();
|
const { externalUrlRegex, replaceSearchResultPathname } =
|
||||||
|
useAlgoliaThemeConfig();
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
(url: string) => {
|
(url: string) => {
|
||||||
|
|
@ -41,8 +45,10 @@ export function useSearchResultUrlProcessor(): (url: string) => string {
|
||||||
// Otherwise => transform to relative URL for SPA navigation
|
// Otherwise => transform to relative URL for SPA navigation
|
||||||
const relativeUrl = `${parsedURL.pathname}${parsedURL.search}${parsedURL.hash}`;
|
const relativeUrl = `${parsedURL.pathname}${parsedURL.search}${parsedURL.hash}`;
|
||||||
|
|
||||||
return withBaseUrl(replacePathname(relativeUrl, replaceSearchResultPathname));
|
return withBaseUrl(
|
||||||
|
replacePathname(relativeUrl, replaceSearchResultPathname)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[withBaseUrl, externalUrlRegex, replaceSearchResultPathname],
|
[withBaseUrl, externalUrlRegex, replaceSearchResultPathname]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,25 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { FacetFilters } from 'algoliasearch/lite';
|
import type { FacetFilters } from 'algoliasearch/lite';
|
||||||
|
|
||||||
export function mergeFacetFilters(f1: FacetFilters, f2: FacetFilters): FacetFilters;
|
export function mergeFacetFilters(
|
||||||
|
f1: FacetFilters,
|
||||||
export function mergeFacetFilters(f1: FacetFilters | undefined, f2: FacetFilters | undefined): FacetFilters | undefined;
|
f2: FacetFilters
|
||||||
|
): FacetFilters;
|
||||||
|
|
||||||
export function mergeFacetFilters(
|
export function mergeFacetFilters(
|
||||||
f1: FacetFilters | undefined,
|
f1: FacetFilters | undefined,
|
||||||
f2: FacetFilters | undefined,
|
f2: FacetFilters | undefined
|
||||||
|
): FacetFilters | undefined;
|
||||||
|
|
||||||
|
export function mergeFacetFilters(
|
||||||
|
f1: FacetFilters | undefined,
|
||||||
|
f2: FacetFilters | undefined
|
||||||
): FacetFilters | undefined {
|
): FacetFilters | undefined {
|
||||||
if (f1 === undefined) {
|
if (f1 === undefined) {
|
||||||
return f2;
|
return f2;
|
||||||
|
|
@ -22,6 +28,7 @@ export function mergeFacetFilters(
|
||||||
return f1;
|
return f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalize = (f: FacetFilters): FacetFilters => (typeof f === 'string' ? [f] : f);
|
const normalize = (f: FacetFilters): FacetFilters =>
|
||||||
|
typeof f === 'string' ? [f] : f;
|
||||||
return [...normalize(f1), ...normalize(f2)];
|
return [...normalize(f1), ...normalize(f2)];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare module '@docsearch/react/modal';
|
|
||||||
declare module '@docsearch/react/style';
|
declare module '@docsearch/react/style';
|
||||||
declare module '@docsearch/react/style/sidepanel';
|
declare module '@docsearch/react/style/sidepanel';
|
||||||
|
|
||||||
|
|
@ -14,6 +13,6 @@ declare module 'eta' {
|
||||||
|
|
||||||
export function compile(
|
export function compile(
|
||||||
template: string,
|
template: string,
|
||||||
options?: Record<string, unknown>,
|
options?: Record<string, unknown>
|
||||||
): (data: Record<string, unknown>, config: Record<string, unknown>) => string;
|
): (data: Record<string, unknown>, config: Record<string, unknown>) => string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { version as docSearchVersion } from '@docsearch/react';
|
|
||||||
|
|
||||||
export const docSearchVersionString = docSearchVersion;
|
|
||||||
|
|
@ -1,26 +1,23 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ThemeConfig, ThemeConfigAlgolia } from '@docsearch/docusaurus-adapter';
|
import type {
|
||||||
|
ThemeConfig,
|
||||||
|
ThemeConfigDocSearch,
|
||||||
|
} from '@docsearch/docusaurus-adapter';
|
||||||
|
|
||||||
export function hasLegacyAlgoliaConfig(themeConfig: ThemeConfig): boolean {
|
export function getDocSearchConfig(
|
||||||
return Boolean(themeConfig.algolia);
|
themeConfig: ThemeConfig
|
||||||
}
|
): ThemeConfigDocSearch {
|
||||||
|
|
||||||
export function getDocSearchConfig(themeConfig: ThemeConfig): ThemeConfigAlgolia {
|
|
||||||
if (themeConfig.docsearch) {
|
if (themeConfig.docsearch) {
|
||||||
return themeConfig.docsearch;
|
return themeConfig.docsearch;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (themeConfig.algolia) {
|
|
||||||
return themeConfig.algolia;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'No DocSearch config found. Please provide "themeConfig.docsearch" (preferred) or "themeConfig.algolia" (legacy).',
|
'No DocSearch config found. Please provide "themeConfig.docsearch".'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,30 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ThemeConfig } from '@docsearch/docusaurus-adapter';
|
import type { ThemeConfig } from '@docsearch/docusaurus-adapter';
|
||||||
import { readDefaultCodeTranslationMessages } from '@docusaurus/theme-translations';
|
import { readDefaultCodeTranslationMessages } from '@docusaurus/theme-translations';
|
||||||
import type { LoadContext, Plugin } from '@docusaurus/types';
|
import type { LoadContext, Plugin } from '@docusaurus/types';
|
||||||
|
|
||||||
import { getDocSearchConfig, hasLegacyAlgoliaConfig } from './getDocSearchConfig';
|
import { getDocSearchConfig } from './getDocSearchConfig';
|
||||||
import { createOpenSearchFile, createOpenSearchHeadTags, shouldCreateOpenSearchFile } from './opensearch';
|
import {
|
||||||
|
createOpenSearchFile,
|
||||||
|
createOpenSearchHeadTags,
|
||||||
|
shouldCreateOpenSearchFile,
|
||||||
|
} from './opensearch';
|
||||||
import { normalizeUrl } from './utils';
|
import { normalizeUrl } from './utils';
|
||||||
|
|
||||||
function hasClassicPreset(context: LoadContext): boolean {
|
|
||||||
return (context.siteConfig.presets ?? []).some((preset) => {
|
|
||||||
if (typeof preset === 'string') {
|
|
||||||
return preset === 'classic' || preset === '@docusaurus/preset-classic';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(preset)) {
|
|
||||||
return preset[0] === 'classic' || preset[0] === '@docusaurus/preset-classic';
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function themeSearchAlgolia(context: LoadContext): Plugin<void> {
|
export default function themeSearchAlgolia(context: LoadContext): Plugin<void> {
|
||||||
const {
|
const {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
siteConfig: { themeConfig },
|
siteConfig: { themeConfig },
|
||||||
i18n: { currentLocale },
|
i18n: { currentLocale },
|
||||||
} = context;
|
} = context;
|
||||||
const { searchPagePath } = getDocSearchConfig(themeConfig as ThemeConfig);
|
const { searchPage } = getDocSearchConfig(themeConfig as ThemeConfig);
|
||||||
const classicPresetWithLegacyAlgoliaConfig =
|
const searchPagePath = searchPage === false ? false : searchPage.path;
|
||||||
hasClassicPreset(context) && hasLegacyAlgoliaConfig(themeConfig as ThemeConfig);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'docsearch-docusaurus-algolia-search',
|
name: 'docsearch-docusaurus-algolia-search',
|
||||||
|
|
@ -55,9 +44,7 @@ export default function themeSearchAlgolia(context: LoadContext): Plugin<void> {
|
||||||
},
|
},
|
||||||
|
|
||||||
contentLoaded({ actions: { addRoute } }) {
|
contentLoaded({ actions: { addRoute } }) {
|
||||||
// The classic preset adds /search through @docusaurus/theme-search-algolia,
|
if (searchPagePath) {
|
||||||
// but only when the legacy "themeConfig.algolia" key is used.
|
|
||||||
if (searchPagePath && !classicPresetWithLegacyAlgoliaConfig) {
|
|
||||||
addRoute({
|
addRoute({
|
||||||
path: normalizeUrl([baseUrl, searchPagePath]),
|
path: normalizeUrl([baseUrl, searchPagePath]),
|
||||||
component: '@theme/SearchPage',
|
component: '@theme/SearchPage',
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
@ -11,13 +11,20 @@ import type { ThemeConfig } from '@docsearch/docusaurus-adapter';
|
||||||
import type { HtmlTags, LoadContext } from '@docusaurus/types';
|
import type { HtmlTags, LoadContext } from '@docusaurus/types';
|
||||||
import { defaultConfig, compile } from 'eta';
|
import { defaultConfig, compile } from 'eta';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
import { getDocSearchConfig } from './getDocSearchConfig';
|
import { getDocSearchConfig } from './getDocSearchConfig';
|
||||||
import openSearchTemplate from './templates/opensearch';
|
import openSearchTemplate from './templates/opensearch';
|
||||||
import { normalizeUrl } from './utils';
|
import { normalizeUrl } from './utils';
|
||||||
|
|
||||||
const getCompiledOpenSearchTemplate = _.memoize(() => compile(openSearchTemplate.trim()));
|
let compiledOpenSearchTemplate: ReturnType<typeof compile> | null = null;
|
||||||
|
|
||||||
|
function getCompiledOpenSearchTemplate(): ReturnType<typeof compile> {
|
||||||
|
if (!compiledOpenSearchTemplate) {
|
||||||
|
compiledOpenSearchTemplate = compile(openSearchTemplate.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
return compiledOpenSearchTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
function renderOpenSearchTemplate(data: {
|
function renderOpenSearchTemplate(data: {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -31,14 +38,19 @@ function renderOpenSearchTemplate(data: {
|
||||||
|
|
||||||
const OPEN_SEARCH_FILENAME = 'opensearch.xml';
|
const OPEN_SEARCH_FILENAME = 'opensearch.xml';
|
||||||
|
|
||||||
export function shouldCreateOpenSearchFile({ context }: { context: LoadContext }): boolean {
|
export function shouldCreateOpenSearchFile({
|
||||||
|
context,
|
||||||
|
}: {
|
||||||
|
context: LoadContext;
|
||||||
|
}): boolean {
|
||||||
const {
|
const {
|
||||||
siteConfig: {
|
siteConfig: {
|
||||||
themeConfig,
|
themeConfig,
|
||||||
future: { experimental_router: router },
|
future: { experimental_router: router },
|
||||||
},
|
},
|
||||||
} = context;
|
} = context;
|
||||||
const { searchPagePath } = getDocSearchConfig(themeConfig as ThemeConfig);
|
const { searchPage } = getDocSearchConfig(themeConfig as ThemeConfig);
|
||||||
|
const searchPagePath = searchPage === false ? false : searchPage.path;
|
||||||
|
|
||||||
return Boolean(searchPagePath) && router !== 'hash';
|
return Boolean(searchPagePath) && router !== 'hash';
|
||||||
}
|
}
|
||||||
|
|
@ -65,14 +77,19 @@ function createOpenSearchFileContent({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createOpenSearchFile({ context }: { context: LoadContext }): Promise<void> {
|
export async function createOpenSearchFile({
|
||||||
|
context,
|
||||||
|
}: {
|
||||||
|
context: LoadContext;
|
||||||
|
}): Promise<void> {
|
||||||
const {
|
const {
|
||||||
outDir,
|
outDir,
|
||||||
siteConfig: { themeConfig },
|
siteConfig: { themeConfig },
|
||||||
} = context;
|
} = context;
|
||||||
const { searchPagePath } = getDocSearchConfig(themeConfig as ThemeConfig);
|
const { searchPage } = getDocSearchConfig(themeConfig as ThemeConfig);
|
||||||
|
const searchPagePath = searchPage === false ? false : searchPage.path;
|
||||||
if (!searchPagePath) {
|
if (!searchPagePath) {
|
||||||
throw new Error('no searchPagePath provided in themeConfig.docsearch or themeConfig.algolia');
|
throw new Error('no searchPage.path provided in themeConfig.docsearch');
|
||||||
}
|
}
|
||||||
const fileContent = createOpenSearchFileContent({ context, searchPagePath });
|
const fileContent = createOpenSearchFileContent({ context, searchPagePath });
|
||||||
try {
|
try {
|
||||||
|
|
@ -84,7 +101,11 @@ export async function createOpenSearchFile({ context }: { context: LoadContext }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOpenSearchHeadTags({ context }: { context: LoadContext }): HtmlTags {
|
export function createOpenSearchHeadTags({
|
||||||
|
context,
|
||||||
|
}: {
|
||||||
|
context: LoadContext;
|
||||||
|
}): HtmlTags {
|
||||||
const {
|
const {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
siteConfig: { title },
|
siteConfig: { title },
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default `
|
export default `
|
||||||
|
|
|
||||||
|
|
@ -1,97 +1,120 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare module '@docsearch/docusaurus-adapter' {
|
declare module '@docsearch/docusaurus-adapter' {
|
||||||
import type { DocSearchProps } from '@docsearch/react';
|
import type {
|
||||||
|
AgentStudioSearchParameters,
|
||||||
|
DocSearchAskAi,
|
||||||
|
DocSearchProps,
|
||||||
|
ToolCalls,
|
||||||
|
} from '@docsearch/react';
|
||||||
import type { SidepanelProps } from '@docsearch/react/sidepanel';
|
import type { SidepanelProps } from '@docsearch/react/sidepanel';
|
||||||
import type { FacetFilters } from 'algoliasearch/lite';
|
|
||||||
import type { DeepPartial, Overwrite, Optional } from 'utility-types';
|
|
||||||
|
|
||||||
type AskAiSearchParameters = {
|
type DocusaurusSidePanelConfig =
|
||||||
facetFilters?: FacetFilters;
|
| boolean
|
||||||
filters?: string;
|
| (Omit<SidepanelProps, 'tools'> & { hideButton?: boolean });
|
||||||
attributesToRetrieve?: string[];
|
type DocusaurusSearchBarSidePanelProps =
|
||||||
restrictSearchableAttributes?: string[];
|
| boolean
|
||||||
distinct?: boolean | number | string;
|
| (Omit<SidepanelProps, 'tools'> & {
|
||||||
|
hideButton?: boolean;
|
||||||
|
tools?: ToolCalls;
|
||||||
|
});
|
||||||
|
|
||||||
|
type SearchPageFacetConfig = {
|
||||||
|
/**
|
||||||
|
* Algolia attribute to build a refinement list from (e.g.
|
||||||
|
* `hierarchy.lvl0`).
|
||||||
|
*/
|
||||||
|
attribute: string;
|
||||||
|
/** Human-readable label displayed above the refinement list. */
|
||||||
|
label?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AgentStudioSearchParameters = Record<string, Omit<AskAiSearchParameters, 'facetFilters'>>;
|
type SearchPageConfig =
|
||||||
|
| false
|
||||||
|
| {
|
||||||
|
path?: string;
|
||||||
|
/**
|
||||||
|
* Facets exposed as refinement lists in the search page sidebar.
|
||||||
|
* Defaults to a single "Section" facet built from `hierarchy.lvl0`.
|
||||||
|
*/
|
||||||
|
facets?: SearchPageFacetConfig[];
|
||||||
|
};
|
||||||
|
|
||||||
// The config after normalization (e.g. AskAI string -> object)
|
|
||||||
// This matches DocSearch v4.3+ AskAi configuration
|
|
||||||
export type AskAiConfig = {
|
export type AskAiConfig = {
|
||||||
indexName: string;
|
agentId: DocSearchAskAi['agentId'];
|
||||||
apiKey: string;
|
suggestedQuestions?: DocSearchAskAi['suggestedQuestions'];
|
||||||
appId: string;
|
searchParameters?: AgentStudioSearchParameters;
|
||||||
assistantId: string;
|
indices?: string[];
|
||||||
suggestedQuestions?: boolean;
|
memory?: DocSearchAskAi['memory'];
|
||||||
useStagingEnv?: boolean;
|
promptSuggestions?: DocSearchAskAi['promptSuggestions'];
|
||||||
sidePanel?: boolean | (SidepanelProps & { hideButton?: boolean });
|
};
|
||||||
} & (
|
|
||||||
| {
|
export type DocusaurusSearchBarAskAiProps = AskAiConfig &
|
||||||
agentStudio: false;
|
Pick<DocSearchAskAi, 'tools'>;
|
||||||
searchParameters?: AskAiSearchParameters;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
agentStudio: true;
|
|
||||||
searchParameters?: AgentStudioSearchParameters;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
agentStudio?: never;
|
|
||||||
searchParameters?: AskAiSearchParameters;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// DocSearch props that Docusaurus exposes directly through props forwarding
|
// DocSearch props that Docusaurus exposes directly through props forwarding
|
||||||
type DocusaurusDocSearchProps = Pick<
|
type DocusaurusDocSearchProps = Pick<
|
||||||
DocSearchProps,
|
DocSearchProps,
|
||||||
'apiKey' | 'appId' | 'indexName' | 'initialQuery' | 'insights' | 'placeholder' | 'searchParameters' | 'translations'
|
| 'apiKey'
|
||||||
|
| 'appId'
|
||||||
|
| 'disableUserPersonalization'
|
||||||
|
| 'facets'
|
||||||
|
| 'getMissingResultsUrl'
|
||||||
|
| 'indices'
|
||||||
|
| 'initialQuery'
|
||||||
|
| 'insights'
|
||||||
|
| 'keyboardShortcuts'
|
||||||
|
| 'maxResultsPerGroup'
|
||||||
|
| 'placeholder'
|
||||||
|
| 'recentSearchesLimit'
|
||||||
|
| 'recentSearchesWithFavoritesLimit'
|
||||||
|
| 'resultBadgeKey'
|
||||||
|
| 'translations'
|
||||||
> & {
|
> & {
|
||||||
// Docusaurus normalizes the AskAI config to an object
|
indices: NonNullable<DocSearchProps['indices']>;
|
||||||
askAi?: AskAiConfig;
|
askAi?: AskAiConfig;
|
||||||
|
sidePanel?: DocusaurusSidePanelConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ThemeConfigAlgolia = DocusaurusDocSearchProps & {
|
export type ThemeConfigDocSearch = DocusaurusDocSearchProps & {
|
||||||
indexName: string;
|
|
||||||
|
|
||||||
// Docusaurus custom options, not coming from DocSearch
|
// Docusaurus custom options, not coming from DocSearch
|
||||||
contextualSearch: boolean;
|
contextualSearch: boolean;
|
||||||
externalUrlRegex?: string;
|
externalUrlRegex?: string;
|
||||||
searchPagePath: string | false | null;
|
searchPage: SearchPageConfig;
|
||||||
replaceSearchResultPathname?: {
|
replaceSearchResultPathname?: {
|
||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type UserDocSearchConfig = Overwrite<
|
export type DocusaurusSearchBarProps = Partial<
|
||||||
DeepPartial<ThemeConfigAlgolia>,
|
Omit<ThemeConfigDocSearch, 'askAi' | 'sidePanel'> & {
|
||||||
{
|
askAi?: DocusaurusSearchBarAskAiProps;
|
||||||
// Required fields:
|
sidePanel?: DocusaurusSearchBarSidePanelProps;
|
||||||
appId: ThemeConfigAlgolia['appId'];
|
|
||||||
apiKey: ThemeConfigAlgolia['apiKey'];
|
|
||||||
indexName: ThemeConfigAlgolia['indexName'];
|
|
||||||
// askAi also accepts a shorter string form
|
|
||||||
askAi?: Optional<AskAiConfig, 'apiKey' | 'appId' | 'indexName'> | string;
|
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
type UserDocSearchConfig = Omit<
|
||||||
|
Partial<ThemeConfigDocSearch>,
|
||||||
|
'apiKey' | 'appId' | 'askAi' | 'indices'
|
||||||
|
> & {
|
||||||
|
appId: ThemeConfigDocSearch['appId'];
|
||||||
|
apiKey: ThemeConfigDocSearch['apiKey'];
|
||||||
|
indices: ThemeConfigDocSearch['indices'];
|
||||||
|
askAi?: AskAiConfig;
|
||||||
|
};
|
||||||
|
|
||||||
export type ThemeConfig = {
|
export type ThemeConfig = {
|
||||||
// Preferred key.
|
docsearch?: ThemeConfigDocSearch;
|
||||||
docsearch?: ThemeConfigAlgolia;
|
|
||||||
// Backward-compatible alias.
|
|
||||||
algolia?: ThemeConfigAlgolia;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserThemeConfig = {
|
export type UserThemeConfig = {
|
||||||
// Preferred key.
|
|
||||||
docsearch?: UserDocSearchConfig;
|
docsearch?: UserDocSearchConfig;
|
||||||
// Backward-compatible alias.
|
|
||||||
algolia?: UserDocSearchConfig;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,9 +125,12 @@ declare module '@theme/SearchPage' {
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/SearchBar' {
|
declare module '@theme/SearchBar' {
|
||||||
|
import type { DocusaurusSearchBarProps } from '@docsearch/docusaurus-adapter';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
export default function SearchBar(): ReactNode;
|
export default function SearchBar(
|
||||||
|
props?: DocusaurusSearchBarProps
|
||||||
|
): ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/SearchTranslations' {
|
declare module '@theme/SearchTranslations' {
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,34 @@
|
||||||
|
/* eslint-disable import/dynamic-import-chunkname */
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { AutocompleteState } from '@algolia/autocomplete-core';
|
import type { AutocompleteState } from '@algolia/autocomplete-core';
|
||||||
import type { ThemeConfigAlgolia } from '@docsearch/docusaurus-adapter';
|
import { DocSearch as DocSearchProvider, useDocSearch } from '@docsearch/core';
|
||||||
import type {
|
import type {
|
||||||
InternalDocSearchHit,
|
DocusaurusSearchBarAskAiProps,
|
||||||
DocSearchModal as DocSearchModalType,
|
ThemeConfigDocSearch,
|
||||||
DocSearchModalProps,
|
} from '@docsearch/docusaurus-adapter';
|
||||||
StoredDocSearchHit,
|
import type { DocSearchAskAiModal as DocSearchAskAiModalType } from '@docsearch/modal/askai';
|
||||||
DocSearchTransformClient,
|
import { DocSearchButton } from '@docsearch/modal/button';
|
||||||
|
import type { DocSearchModal as DocSearchModalType } from '@docsearch/modal/modal';
|
||||||
|
import type {
|
||||||
|
DocSearchAskAiModalProps,
|
||||||
DocSearchHit,
|
DocSearchHit,
|
||||||
|
DocSearchModalProps,
|
||||||
|
DocSearchProps,
|
||||||
|
DocSearchTransformClient,
|
||||||
DocSearchTranslations,
|
DocSearchTranslations,
|
||||||
|
InternalDocSearchHit,
|
||||||
|
StoredDocSearchHit,
|
||||||
|
ToolCalls,
|
||||||
} from '@docsearch/react';
|
} from '@docsearch/react';
|
||||||
import { DocSearchButton } from '@docsearch/react/button';
|
import { SidepanelButton } from '@docsearch/sidepanel/button';
|
||||||
import { SidepanelButton } from '@docsearch/react/sidepanel';
|
import type { Sidepanel as SidepanelType } from '@docsearch/sidepanel/sidepanel';
|
||||||
import type { Sidepanel as SidepanelType } from '@docsearch/react/sidepanel';
|
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||||
import { useDocSearchKeyboardEvents } from '@docsearch/react/useDocSearchKeyboardEvents';
|
|
||||||
import Head from '@docusaurus/Head';
|
import Head from '@docusaurus/Head';
|
||||||
import Link from '@docusaurus/Link';
|
import Link from '@docusaurus/Link';
|
||||||
import { useHistory } from '@docusaurus/router';
|
import { useHistory } from '@docusaurus/router';
|
||||||
|
|
@ -28,74 +37,100 @@ import Translate from '@docusaurus/Translate';
|
||||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
import translations from '@theme/SearchTranslations';
|
import translations from '@theme/SearchTranslations';
|
||||||
import type { FacetFilters } from 'algoliasearch/lite';
|
import React, {
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
useCallback,
|
||||||
import { createPortal } from 'react-dom';
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
type JSX,
|
||||||
|
type ReactNode,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
mergeFacetFilters,
|
||||||
|
useAlgoliaAskAi,
|
||||||
|
useAlgoliaAskAiSidepanel,
|
||||||
useAlgoliaContextualFacetFilters,
|
useAlgoliaContextualFacetFilters,
|
||||||
useAlgoliaThemeConfig,
|
useAlgoliaThemeConfig,
|
||||||
useSearchResultUrlProcessor,
|
useSearchResultUrlProcessor,
|
||||||
useAlgoliaAskAi,
|
|
||||||
useAlgoliaAskAiSidepanel,
|
|
||||||
mergeFacetFilters,
|
|
||||||
} from '../../client';
|
} from '../../client';
|
||||||
|
|
||||||
type DocSearchProps = Omit<DocSearchModalProps, 'initialScrollY' | 'onClose'> & {
|
type NavigatorNavigateParams = Parameters<
|
||||||
contextualSearch?: string;
|
NonNullable<NonNullable<DocSearchModalProps['navigator']>['navigate']>
|
||||||
|
>[0];
|
||||||
|
|
||||||
|
type SidePanelOptions = Exclude<
|
||||||
|
NonNullable<ThemeConfigDocSearch['sidePanel']>,
|
||||||
|
boolean
|
||||||
|
> & { tools?: ToolCalls };
|
||||||
|
type SidePanelPanelOptions = Omit<
|
||||||
|
SidePanelOptions,
|
||||||
|
'hideButton' | 'keyboardShortcuts'
|
||||||
|
>;
|
||||||
|
type AskAiOptions = NonNullable<ThemeConfigDocSearch['askAi']> &
|
||||||
|
Pick<DocusaurusSearchBarAskAiProps, 'tools'>;
|
||||||
|
|
||||||
|
type AdapterDocSearchProps = Omit<
|
||||||
|
DocSearchAskAiModalProps,
|
||||||
|
| 'askAi'
|
||||||
|
| 'initialScrollY'
|
||||||
|
| 'isAskAiActive'
|
||||||
|
| 'isHybridModeSupported'
|
||||||
|
| 'onAskAiToggle'
|
||||||
|
| 'onClose'
|
||||||
|
> & {
|
||||||
|
appId: DocSearchProps['appId'];
|
||||||
|
apiKey: DocSearchProps['apiKey'];
|
||||||
|
askAi?: AskAiOptions;
|
||||||
|
contextualSearch?: boolean;
|
||||||
externalUrlRegex?: string;
|
externalUrlRegex?: string;
|
||||||
searchPagePath: boolean | string;
|
indices: NonNullable<DocSearchProps['indices']>;
|
||||||
askAi?: Exclude<(DocSearchModalProps & { askAi: unknown })['askAi'], string | undefined>;
|
searchPage: ThemeConfigDocSearch['searchPage'];
|
||||||
};
|
sidePanel?: SidePanelOptions | boolean;
|
||||||
|
|
||||||
type AskAiTogglePayload = {
|
|
||||||
query: string;
|
|
||||||
messageId?: string;
|
|
||||||
suggestedQuestionId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type OnAskAiToggle = (toggle: boolean, payload?: AskAiTogglePayload) => void;
|
|
||||||
type NavigatorNavigateParams = Parameters<NonNullable<NonNullable<DocSearchModalProps['navigator']>['navigate']>>[0];
|
|
||||||
|
|
||||||
interface AlgoliaSearchBarProps extends Omit<DocSearchProps, 'askAi'> {
|
|
||||||
indexName: string;
|
|
||||||
askAi?: ThemeConfigAlgolia['askAi'];
|
|
||||||
translations?: DocSearchTranslations;
|
translations?: DocSearchTranslations;
|
||||||
}
|
};
|
||||||
|
|
||||||
let DocSearchModal: typeof DocSearchModalType | null = null;
|
let DocSearchModal: typeof DocSearchModalType | null = null;
|
||||||
|
let DocSearchAskAiModal: typeof DocSearchAskAiModalType | null = null;
|
||||||
let DocSearchSidepanel: typeof SidepanelType | null = null;
|
let DocSearchSidepanel: typeof SidepanelType | null = null;
|
||||||
|
|
||||||
function importDocSearchModalIfNeeded(): Promise<void> {
|
function importDocSearchModalIfNeeded(): Promise<void> {
|
||||||
if (DocSearchModal) {
|
if (DocSearchModal && DocSearchAskAiModal) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line import/dynamic-import-chunkname
|
return Promise.all([
|
||||||
return Promise.all([import('@docsearch/react/modal'), import('@docsearch/react/style'), import('./styles.css')]).then(
|
import('@docsearch/modal/modal'),
|
||||||
([{ DocSearchModal: Modal }]) => {
|
import('@docsearch/modal/askai'),
|
||||||
|
import('@docsearch/react/style'),
|
||||||
|
import('./styles.css'),
|
||||||
|
]).then(
|
||||||
|
([{ DocSearchModal: Modal }, { DocSearchAskAiModal: AskAiModal }]) => {
|
||||||
DocSearchModal = Modal;
|
DocSearchModal = Modal;
|
||||||
},
|
DocSearchAskAiModal = AskAiModal;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importDocSearchSidepanelIfNeeded(): Promise<void> {
|
function importDocSearchSidepanelIfNeeded(): Promise<void> {
|
||||||
await importDocSearchModalIfNeeded();
|
|
||||||
if (DocSearchSidepanel) {
|
if (DocSearchSidepanel) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line import/dynamic-import-chunkname
|
return Promise.all([
|
||||||
return Promise.all([import('@docsearch/react/sidepanel'), import('@docsearch/react/style/sidepanel')]).then(
|
import('@docsearch/sidepanel/sidepanel'),
|
||||||
([{ Sidepanel }]) => {
|
import('@docsearch/react/style/sidepanel'),
|
||||||
DocSearchSidepanel = Sidepanel;
|
]).then(([{ Sidepanel }]) => {
|
||||||
},
|
DocSearchSidepanel = Sidepanel;
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function useNavigator({
|
function useNavigator({
|
||||||
externalUrlRegex,
|
externalUrlRegex,
|
||||||
}: Pick<DocSearchProps, 'externalUrlRegex'>): DocSearchModalProps['navigator'] {
|
}: Pick<
|
||||||
|
AdapterDocSearchProps,
|
||||||
|
'externalUrlRegex'
|
||||||
|
>): DocSearchModalProps['navigator'] {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [navigator] = useState<DocSearchModalProps['navigator']>(() => {
|
const [navigator] = useState<DocSearchModalProps['navigator']>(() => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -122,23 +157,27 @@ function useTransformSearchClient(): DocSearchModalProps['transformSearchClient'
|
||||||
searchClient.addAlgoliaAgent('docusaurus', docusaurusVersion);
|
searchClient.addAlgoliaAgent('docusaurus', docusaurusVersion);
|
||||||
return searchClient;
|
return searchClient;
|
||||||
},
|
},
|
||||||
[docusaurusVersion],
|
[docusaurusVersion]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useTransformItems(props: Pick<DocSearchProps, 'transformItems'>) {
|
function useTransformItems(
|
||||||
|
props: Pick<AdapterDocSearchProps, 'transformItems'>
|
||||||
|
): DocSearchModalProps['transformItems'] {
|
||||||
const processSearchResultUrl = useSearchResultUrlProcessor();
|
const processSearchResultUrl = useSearchResultUrlProcessor();
|
||||||
const [transformItems] = useState<DocSearchModalProps['transformItems']>(() => {
|
const [transformItems] = useState<DocSearchModalProps['transformItems']>(
|
||||||
return (items: DocSearchHit[]) =>
|
() => {
|
||||||
props.transformItems
|
return (items: DocSearchHit[]) =>
|
||||||
? // Custom transformItems
|
props.transformItems
|
||||||
props.transformItems(items)
|
? // Custom transformItems
|
||||||
: // Default transformItems
|
props.transformItems(items)
|
||||||
items.map((item) => ({
|
: // Default transformItems
|
||||||
...item,
|
items.map((item) => ({
|
||||||
url: processSearchResultUrl(item.url),
|
...item,
|
||||||
}));
|
url: processSearchResultUrl(item.url),
|
||||||
});
|
}));
|
||||||
|
}
|
||||||
|
);
|
||||||
return transformItems;
|
return transformItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,17 +187,29 @@ function useResultsFooterComponent({
|
||||||
}: {
|
}: {
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
searchPagePath?: string;
|
searchPagePath?: string;
|
||||||
}): DocSearchProps['resultsFooterComponent'] {
|
}): DocSearchModalProps['resultsFooterComponent'] {
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() =>
|
() =>
|
||||||
searchPagePath
|
searchPagePath
|
||||||
? ({ state }) => <ResultsFooter state={state} searchPagePath={searchPagePath} onClose={closeModal} />
|
? ({ state }) => (
|
||||||
|
<ResultsFooter
|
||||||
|
state={state}
|
||||||
|
searchPagePath={searchPagePath}
|
||||||
|
onClose={closeModal}
|
||||||
|
/>
|
||||||
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
[closeModal, searchPagePath],
|
[closeModal, searchPagePath]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Hit({ hit, children }: { hit: InternalDocSearchHit | StoredDocSearchHit; children: ReactNode }) {
|
function Hit({
|
||||||
|
hit,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
hit: InternalDocSearchHit | StoredDocSearchHit;
|
||||||
|
children: ReactNode;
|
||||||
|
}): JSX.Element {
|
||||||
return <Link to={hit.url}>{children}</Link>;
|
return <Link to={hit.url}>{children}</Link>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,7 +219,11 @@ type ResultsFooterProps = {
|
||||||
searchPagePath: string;
|
searchPagePath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ResultsFooter({ state, onClose, searchPagePath }: ResultsFooterProps) {
|
function ResultsFooter({
|
||||||
|
state,
|
||||||
|
onClose,
|
||||||
|
searchPagePath,
|
||||||
|
}: ResultsFooterProps): JSX.Element {
|
||||||
const searchPageLink = useBaseUrl(searchPagePath);
|
const searchPageLink = useBaseUrl(searchPagePath);
|
||||||
const nbHits = (state.context as { nbHits?: number }).nbHits ?? 0;
|
const nbHits = (state.context as { nbHits?: number }).nbHits ?? 0;
|
||||||
const searchLink = state.query
|
const searchLink = state.query
|
||||||
|
|
@ -184,125 +239,140 @@ function ResultsFooter({ state, onClose, searchPagePath }: ResultsFooterProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useSearchParameters({ contextualSearch, ...props }: DocSearchProps): DocSearchProps['searchParameters'] {
|
function useSearchIndices({
|
||||||
|
contextualSearch,
|
||||||
|
indices,
|
||||||
|
}: Pick<
|
||||||
|
AdapterDocSearchProps,
|
||||||
|
'contextualSearch' | 'indices'
|
||||||
|
>): AdapterDocSearchProps['indices'] {
|
||||||
const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters();
|
const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters();
|
||||||
|
|
||||||
const configFacetFilters: FacetFilters = props.searchParameters?.facetFilters ?? [];
|
return useMemo(() => {
|
||||||
|
if (!contextualSearch) {
|
||||||
|
return indices;
|
||||||
|
}
|
||||||
|
|
||||||
const facetFilters: FacetFilters = contextualSearch
|
return indices.map((index) => {
|
||||||
? // Merge contextual search filters with config filters
|
if (typeof index === 'string') {
|
||||||
mergeFacetFilters(contextualSearchFacetFilters, configFacetFilters)
|
return {
|
||||||
: // ... or use config facetFilters
|
name: index,
|
||||||
configFacetFilters;
|
searchParameters: {
|
||||||
|
facetFilters: contextualSearchFacetFilters,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// We let users override default searchParameters if they want to
|
return {
|
||||||
return {
|
...index,
|
||||||
...props.searchParameters,
|
searchParameters: {
|
||||||
facetFilters,
|
...index.searchParameters,
|
||||||
};
|
facetFilters: mergeFacetFilters(
|
||||||
|
index.searchParameters?.facetFilters,
|
||||||
|
contextualSearchFacetFilters
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [contextualSearch, contextualSearchFacetFilters, indices]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DocSearch({ externalUrlRegex, ...props }: AlgoliaSearchBarProps) {
|
function getSearchPagePath(
|
||||||
|
searchPage: ThemeConfigDocSearch['searchPage']
|
||||||
|
): string | undefined {
|
||||||
|
return searchPage === false ? undefined : searchPage.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSidePanelPanelOptions(
|
||||||
|
sidePanelOptions?: SidePanelOptions
|
||||||
|
): SidePanelPanelOptions {
|
||||||
|
if (!sidePanelOptions) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
hideButton: _hideButton,
|
||||||
|
keyboardShortcuts: _keyboardShortcuts,
|
||||||
|
...panelOptions
|
||||||
|
} = sidePanelOptions;
|
||||||
|
return panelOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DocSearch({
|
||||||
|
askAi,
|
||||||
|
contextualSearch,
|
||||||
|
externalUrlRegex,
|
||||||
|
searchPage,
|
||||||
|
sidePanel,
|
||||||
|
...props
|
||||||
|
}: AdapterDocSearchProps) {
|
||||||
const navigator = useNavigator({ externalUrlRegex });
|
const navigator = useNavigator({ externalUrlRegex });
|
||||||
const searchParameters = useSearchParameters({ ...props } as DocSearchProps);
|
const indices = useSearchIndices({
|
||||||
|
contextualSearch,
|
||||||
|
indices: props.indices,
|
||||||
|
});
|
||||||
const transformItems = useTransformItems(props);
|
const transformItems = useTransformItems(props);
|
||||||
const transformSearchClient = useTransformSearchClient();
|
const transformSearchClient = useTransformSearchClient();
|
||||||
|
const { closeModal, isModalActive } = useDocSearch();
|
||||||
const searchContainer = useRef<HTMLDivElement | null>(null);
|
const [modalLoaded, setModalLoaded] = useState(
|
||||||
const searchButtonRef = useRef<HTMLButtonElement | null>(null);
|
Boolean(DocSearchModal && DocSearchAskAiModal)
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
);
|
||||||
const [initialQuery, setInitialQuery] = useState<string | undefined>(undefined);
|
const [sidepanelLoaded, setSidepanelLoaded] = useState(
|
||||||
|
Boolean(DocSearchSidepanel)
|
||||||
const { isAskAiActive, currentPlaceholder, onAskAiToggle, extraAskAiProps, askAi } = useAlgoliaAskAi(props);
|
);
|
||||||
const {
|
const { modalAskAi, sidePanelAskAi } = useAlgoliaAskAi({
|
||||||
sidePanelEnabled,
|
appId: props.appId,
|
||||||
showSidepanelButton,
|
apiKey: props.apiKey,
|
||||||
sidePanelOptions,
|
indices: props.indices,
|
||||||
sidePanelAgentStudio,
|
askAi,
|
||||||
sidepanelPortalContainer,
|
|
||||||
isSidepanelOpen,
|
|
||||||
sidepanelInitialMessage,
|
|
||||||
openSidepanel,
|
|
||||||
closeSidepanel,
|
|
||||||
toggleSidepanel,
|
|
||||||
handleSidepanelOpen,
|
|
||||||
loadSidepanel,
|
|
||||||
} = useAlgoliaAskAiSidepanel({
|
|
||||||
askAiConfig: askAi,
|
|
||||||
importSidepanel: importDocSearchSidepanelIfNeeded,
|
|
||||||
});
|
});
|
||||||
|
const { sidePanelEnabled, showSidepanelButton, sidePanelOptions } =
|
||||||
const prepareSearchContainer = useCallback(() => {
|
useAlgoliaAskAiSidepanel({
|
||||||
if (!searchContainer.current) {
|
sidePanel,
|
||||||
const divElement = document.createElement('div');
|
});
|
||||||
searchContainer.current = divElement;
|
const searchPagePath = getSearchPagePath(searchPage);
|
||||||
document.body.insertBefore(divElement, document.body.firstChild);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const openModal = useCallback(() => {
|
|
||||||
prepareSearchContainer();
|
|
||||||
importDocSearchModalIfNeeded().then(() => setIsOpen(true));
|
|
||||||
}, [prepareSearchContainer]);
|
|
||||||
|
|
||||||
const closeModal = useCallback(() => {
|
|
||||||
setIsOpen(false);
|
|
||||||
searchButtonRef.current?.focus();
|
|
||||||
setInitialQuery(undefined);
|
|
||||||
onAskAiToggle(false);
|
|
||||||
}, [onAskAiToggle]);
|
|
||||||
|
|
||||||
const handleAskAiToggle = useCallback<OnAskAiToggle>(
|
|
||||||
(active, payload) => {
|
|
||||||
if (active && sidePanelEnabled) {
|
|
||||||
closeModal();
|
|
||||||
openSidepanel(payload);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onAskAiToggle(active);
|
|
||||||
},
|
|
||||||
[closeModal, onAskAiToggle, openSidepanel, sidePanelEnabled],
|
|
||||||
);
|
|
||||||
|
|
||||||
// cleanup search container
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (searchContainer.current) {
|
|
||||||
searchContainer.current.remove();
|
|
||||||
searchContainer.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleInput = useCallback(
|
|
||||||
(event: KeyboardEvent) => {
|
|
||||||
if (event.key === 'f' && (event.metaKey || event.ctrlKey)) {
|
|
||||||
// ignore browser's ctrl+f
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// prevents duplicate key insertion in the modal input
|
|
||||||
event.preventDefault();
|
|
||||||
setInitialQuery(event.key);
|
|
||||||
openModal();
|
|
||||||
},
|
|
||||||
[openModal],
|
|
||||||
);
|
|
||||||
|
|
||||||
const resultsFooterSearchPagePath = typeof props.searchPagePath === 'string' ? props.searchPagePath : undefined;
|
|
||||||
const resultsFooterComponent = useResultsFooterComponent({
|
const resultsFooterComponent = useResultsFooterComponent({
|
||||||
closeModal,
|
closeModal,
|
||||||
searchPagePath: resultsFooterSearchPagePath,
|
searchPagePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
useDocSearchKeyboardEvents({
|
const loadModal = useCallback(() => {
|
||||||
isOpen,
|
return importDocSearchModalIfNeeded().then(() => setModalLoaded(true));
|
||||||
onOpen: openModal,
|
}, []);
|
||||||
onClose: closeModal,
|
|
||||||
onInput: handleInput,
|
const loadSidepanel = useCallback(() => {
|
||||||
searchButtonRef,
|
return importDocSearchSidepanelIfNeeded().then(() =>
|
||||||
isAskAiActive: isAskAiActive ?? false,
|
setSidepanelLoaded(true)
|
||||||
onAskAiToggle: onAskAiToggle ?? (() => {}),
|
);
|
||||||
});
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isModalActive) {
|
||||||
|
loadModal();
|
||||||
|
}
|
||||||
|
}, [isModalActive, loadModal]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sidePanelEnabled && sidePanelAskAi) {
|
||||||
|
loadSidepanel();
|
||||||
|
}
|
||||||
|
}, [loadSidepanel, sidePanelAskAi, sidePanelEnabled]);
|
||||||
|
|
||||||
|
const modalProps = {
|
||||||
|
...props,
|
||||||
|
navigator,
|
||||||
|
transformItems,
|
||||||
|
hitComponent: Hit,
|
||||||
|
transformSearchClient,
|
||||||
|
...(searchPagePath && {
|
||||||
|
resultsFooterComponent,
|
||||||
|
}),
|
||||||
|
translations: props.translations?.modal ?? translations.modal,
|
||||||
|
indices,
|
||||||
|
};
|
||||||
|
const panelOptions = getSidePanelPanelOptions(
|
||||||
|
typeof sidePanel === 'object' ? sidePanel : undefined
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -310,103 +380,87 @@ function DocSearch({ externalUrlRegex, ...props }: AlgoliaSearchBarProps) {
|
||||||
{/* This hints the browser that the website will load data from Algolia,
|
{/* This hints the browser that the website will load data from Algolia,
|
||||||
and allows it to preconnect to the DocSearch cluster. It makes the first
|
and allows it to preconnect to the DocSearch cluster. It makes the first
|
||||||
query faster, especially on mobile. */}
|
query faster, especially on mobile. */}
|
||||||
<link rel="preconnect" href={`https://${props.appId}-dsn.algolia.net`} crossOrigin="anonymous" />
|
<link
|
||||||
|
rel="preconnect"
|
||||||
|
href={`https://${props.appId}-dsn.algolia.net`}
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<div className="DocSearch-SearchBar">
|
<div className="DocSearch-SearchBar">
|
||||||
<DocSearchButton
|
<DocSearchButton
|
||||||
ref={searchButtonRef}
|
|
||||||
translations={props.translations?.button ?? translations.button}
|
translations={props.translations?.button ?? translations.button}
|
||||||
onTouchStart={importDocSearchModalIfNeeded}
|
onTouchStart={loadModal}
|
||||||
onFocus={importDocSearchModalIfNeeded}
|
onFocus={loadModal}
|
||||||
onMouseOver={importDocSearchModalIfNeeded}
|
onMouseOver={loadModal}
|
||||||
onClick={openModal}
|
|
||||||
/>
|
/>
|
||||||
{showSidepanelButton && (
|
{ExecutionEnvironment.canUseDOM &&
|
||||||
<SidepanelButton
|
showSidepanelButton &&
|
||||||
translations={{
|
sidePanelAskAi && (
|
||||||
buttonText: '',
|
<SidepanelButton
|
||||||
buttonAriaLabel: 'Ask AI',
|
translations={{
|
||||||
}}
|
buttonText: '',
|
||||||
variant={sidePanelOptions?.variant ?? 'inline'}
|
buttonAriaLabel: 'Ask AI',
|
||||||
keyboardShortcuts={sidePanelOptions?.keyboardShortcuts}
|
}}
|
||||||
onTouchStart={loadSidepanel}
|
variant={sidePanelOptions?.variant ?? 'inline'}
|
||||||
onFocus={loadSidepanel}
|
keyboardShortcuts={sidePanelOptions?.keyboardShortcuts}
|
||||||
onMouseOver={loadSidepanel}
|
/>
|
||||||
onClick={toggleSidepanel}
|
)}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isOpen &&
|
{modalLoaded &&
|
||||||
DocSearchModal &&
|
(modalAskAi && DocSearchAskAiModal ? (
|
||||||
searchContainer.current &&
|
<DocSearchAskAiModal {...modalProps} askAi={modalAskAi} />
|
||||||
createPortal(
|
) : (
|
||||||
<DocSearchModal
|
DocSearchModal && <DocSearchModal {...modalProps} />
|
||||||
initialScrollY={window.scrollY}
|
))}
|
||||||
initialQuery={initialQuery}
|
|
||||||
navigator={navigator}
|
|
||||||
transformItems={transformItems}
|
|
||||||
hitComponent={Hit}
|
|
||||||
transformSearchClient={transformSearchClient}
|
|
||||||
interceptAskAiEvent={(payload) => {
|
|
||||||
if (!sidePanelEnabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
closeModal();
|
|
||||||
openSidepanel(payload);
|
|
||||||
return true;
|
|
||||||
}}
|
|
||||||
onClose={closeModal}
|
|
||||||
{...(resultsFooterSearchPagePath && {
|
|
||||||
resultsFooterComponent,
|
|
||||||
})}
|
|
||||||
placeholder={currentPlaceholder}
|
|
||||||
{...(props as DocSearchProps)}
|
|
||||||
translations={props.translations?.modal ?? translations.modal}
|
|
||||||
searchParameters={searchParameters}
|
|
||||||
{...extraAskAiProps}
|
|
||||||
isHybridModeSupported={sidePanelEnabled}
|
|
||||||
onAskAiToggle={handleAskAiToggle as DocSearchModalProps['onAskAiToggle']}
|
|
||||||
/>,
|
|
||||||
searchContainer.current,
|
|
||||||
)}
|
|
||||||
|
|
||||||
{sidePanelEnabled &&
|
{ExecutionEnvironment.canUseDOM &&
|
||||||
DocSearchSidepanel &&
|
sidePanelEnabled &&
|
||||||
askAi &&
|
sidepanelLoaded &&
|
||||||
sidepanelPortalContainer &&
|
sidePanelAskAi &&
|
||||||
createPortal(
|
DocSearchSidepanel && (
|
||||||
<DocSearchSidepanel
|
<DocSearchSidepanel
|
||||||
{...sidePanelOptions}
|
{...panelOptions}
|
||||||
variant={sidePanelOptions?.variant ?? 'inline'}
|
variant={panelOptions.variant ?? 'inline'}
|
||||||
pushSelector={sidePanelOptions?.pushSelector ?? '#__docusaurus'}
|
pushSelector={panelOptions.pushSelector ?? '#__docusaurus'}
|
||||||
assistantId={askAi.assistantId}
|
agentId={sidePanelAskAi.agentId}
|
||||||
apiKey={askAi.apiKey}
|
apiKey={sidePanelAskAi.apiKey}
|
||||||
appId={askAi.appId}
|
appId={sidePanelAskAi.appId}
|
||||||
indexName={askAi.indexName}
|
searchParameters={sidePanelAskAi.searchParameters}
|
||||||
agentStudio={sidePanelAgentStudio}
|
indices={panelOptions.indices ?? sidePanelAskAi.indices}
|
||||||
suggestedQuestions={sidePanelOptions?.suggestedQuestions ?? askAi.suggestedQuestions}
|
suggestedQuestions={
|
||||||
isOpen={isSidepanelOpen}
|
panelOptions.suggestedQuestions ??
|
||||||
initialMessage={sidepanelInitialMessage}
|
sidePanelAskAi.suggestedQuestions
|
||||||
onOpen={handleSidepanelOpen}
|
}
|
||||||
onClose={closeSidepanel}
|
tools={panelOptions.tools ?? sidePanelAskAi.tools}
|
||||||
/>,
|
memory={panelOptions.memory ?? sidePanelAskAi.memory}
|
||||||
sidepanelPortalContainer,
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SearchBar(props: Partial<AlgoliaSearchBarProps>): ReactNode {
|
export default function SearchBar(
|
||||||
|
props: Partial<AdapterDocSearchProps>
|
||||||
|
): ReactNode {
|
||||||
const themeConfig = useAlgoliaThemeConfig();
|
const themeConfig = useAlgoliaThemeConfig();
|
||||||
|
|
||||||
const docSearchProps: AlgoliaSearchBarProps = {
|
const docSearchProps: AdapterDocSearchProps = {
|
||||||
...(themeConfig as unknown as AlgoliaSearchBarProps),
|
...(themeConfig as AdapterDocSearchProps),
|
||||||
// Let props override theme config
|
// Let props override theme config
|
||||||
// See https://github.com/facebook/docusaurus/pull/11581
|
// See https://github.com/facebook/docusaurus/pull/11581
|
||||||
...props,
|
...props,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <DocSearch {...docSearchProps} />;
|
return (
|
||||||
|
<DocSearchProvider
|
||||||
|
appId={docSearchProps.appId}
|
||||||
|
apiKey={docSearchProps.apiKey}
|
||||||
|
initialQuery={docSearchProps.initialQuery}
|
||||||
|
keyboardShortcuts={docSearchProps.keyboardShortcuts}
|
||||||
|
>
|
||||||
|
<DocSearch {...docSearchProps} />
|
||||||
|
</DocSearchProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,54 +6,55 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--docsearch-font-family: var(--ifm-font-family-base);
|
||||||
--docsearch-primary-color: var(--ifm-color-primary);
|
--docsearch-primary-color: var(--ifm-color-primary);
|
||||||
--docsearch-text-color: var(--ifm-font-color-base);
|
--docsearch-text-color: var(--ifm-font-color-base);
|
||||||
--docsearch-border-radius: var(--ifm-global-radius);
|
--docsearch-border-radius: var(--ifm-global-radius);
|
||||||
--docsearch-highlight-color: var(--ifm-color-primary);
|
--docsearch-highlight-color: var(--ifm-color-primary);
|
||||||
--docsearch-soft-primary-color: color-mix(
|
--docsearch-soft-primary-color: color-mix(in srgb,
|
||||||
in srgb,
|
var(--ifm-color-primary) 12%,
|
||||||
var(--ifm-color-primary) 12%,
|
transparent);
|
||||||
transparent
|
|
||||||
);
|
|
||||||
--docsearch-focus-color: var(--ifm-color-primary-dark);
|
--docsearch-focus-color: var(--ifm-color-primary-dark);
|
||||||
--docsearch-subtle-color: var(--ifm-color-emphasis-200);
|
--docsearch-subtle-color: var(--ifm-color-emphasis-200);
|
||||||
--docsearch-secondary-text-color: var(--ifm-color-emphasis-600);
|
--docsearch-secondary-text-color: var(--ifm-color-secondary-darker);
|
||||||
--docsearch-icon-color: var(--ifm-color-emphasis-600);
|
--docsearch-icon-color: var(--ifm-color-secondary-darker);
|
||||||
--docsearch-muted-color: var(--ifm-color-emphasis-600);
|
--docsearch-muted-color: var(--ifm-color-secondary-darker);
|
||||||
--docsearch-container-background: rgb(0 0 0 / 60%);
|
--docsearch-container-background: rgb(0 0 0 / 60%);
|
||||||
--docsearch-background-color: var(--ifm-color-emphasis-200);
|
--docsearch-background-color: var(--ifm-color-secondary);
|
||||||
|
|
||||||
/* Modal */
|
/* Modal */
|
||||||
--docsearch-modal-background: var(--ifm-color-emphasis-100);
|
--docsearch-modal-background: var(--ifm-background-color);
|
||||||
--docsearch-modal-shadow: var(--ifm-global-shadow-md);
|
--docsearch-modal-shadow: var(--ifm-global-shadow-md);
|
||||||
|
|
||||||
/* Button */
|
/* Button */
|
||||||
--docsearch-search-button-background: var(--ifm-background-color);
|
--docsearch-search-button-background: var(--ifm-background-color);
|
||||||
--docsearch-search-button-text-color: var(--ifm-color-emphasis-600);
|
--docsearch-search-button-text-color: var(--ifm-color-secondary-darker);
|
||||||
|
|
||||||
/* Search box */
|
/* Search box */
|
||||||
--docsearch-searchbox-background: var(--ifm-background-color);
|
--docsearch-searchbox-background: var(--ifm-background-color);
|
||||||
--docsearch-searchbox-focus-background: var(--ifm-background-surface-color);
|
--docsearch-searchbox-focus-background: var(--ifm-background-color);
|
||||||
|
|
||||||
/* Hit */
|
/* Hit */
|
||||||
--docsearch-hit-color: var(--ifm-font-color-base);
|
--docsearch-hit-color: var(--ifm-font-color-base);
|
||||||
--docsearch-hit-highlight-color: color-mix(
|
--docsearch-hit-highlight-color: color-mix(in srgb,
|
||||||
in srgb,
|
var(--ifm-color-primary) 12%,
|
||||||
var(--ifm-color-primary) 12%,
|
transparent);
|
||||||
transparent
|
--docsearch-hit-background: var(--ifm-background-color);
|
||||||
);
|
--docsearch-hit-focus-background: var(--ifm-color-secondary);
|
||||||
--docsearch-hit-active-color: var(--ifm-color-white);
|
--docsearch-hit-active-color: var(--ifm-color-white);
|
||||||
--docsearch-hit-background: var(--ifm-background-surface-color);
|
|
||||||
--docsearch-key-background: var(--ifm-color-emphasis-200);
|
--docsearch-key-background: var(--ifm-color-emphasis-200);
|
||||||
--docsearch-key-color: var(--ifm-color-emphasis-600);
|
--docsearch-key-color: var(--ifm-color-secondary-darker);
|
||||||
--docsearch-key-pressed-shadow: inset 0 2px 4px rgb(0 0 0 / 12%);
|
--docsearch-key-pressed-shadow: inset 0 2px 4px rgb(0 0 0 / 12%);
|
||||||
|
|
||||||
/* Footer */
|
/* Footer */
|
||||||
--docsearch-footer-background: var(--ifm-background-surface-color);
|
--docsearch-footer-background: var(--ifm-background-color);
|
||||||
--docsearch-footer-shadow: 0 -1px 0 0 var(--ifm-color-emphasis-200);
|
--docsearch-footer-shadow: 0 -1px 0 0 var(--ifm-color-emphasis-200);
|
||||||
--docsearch-key-gradient: linear-gradient(
|
--docsearch-key-gradient: linear-gradient(-26.5deg,
|
||||||
-26.5deg,
|
var(--ifm-color-emphasis-300) 0%,
|
||||||
var(--ifm-color-emphasis-300) 0%,
|
var(--ifm-color-emphasis-200) 100%);
|
||||||
var(--ifm-color-emphasis-200) 100%
|
|
||||||
);
|
|
||||||
|
|
||||||
/* Sidepanel */
|
/* Sidepanel */
|
||||||
--docsearch-sidepanel-background: var(--ifm-color-emphasis-100);
|
--docsearch-sidepanel-background: var(--ifm-background-color);
|
||||||
--docsearch-sidepanel-background-dark: var(--docsearch-searchbox-background);
|
--docsearch-sidepanel-background-dark: var(--docsearch-searchbox-background);
|
||||||
--docsearch-sidepanel-white: var(--ifm-background-color);
|
--docsearch-sidepanel-white: var(--ifm-background-color);
|
||||||
--docsearch-sidepanel-primary: var(--docsearch-primary-color);
|
--docsearch-sidepanel-primary: var(--docsearch-primary-color);
|
||||||
|
|
@ -68,15 +69,13 @@
|
||||||
--docsearch-sidepanel-scrollbar-bg: var(--docsearch-modal-background);
|
--docsearch-sidepanel-scrollbar-bg: var(--docsearch-modal-background);
|
||||||
--docsearch-sidepanel-hit-background: var(--docsearch-hit-background);
|
--docsearch-sidepanel-hit-background: var(--docsearch-hit-background);
|
||||||
--docsearch-sidepanel-hit-color: var(--docsearch-hit-color);
|
--docsearch-sidepanel-hit-color: var(--docsearch-hit-color);
|
||||||
--docsearch-sidepanel-hit-highlight-color: var(
|
--docsearch-sidepanel-hit-highlight-color: var(--docsearch-hit-highlight-color);
|
||||||
--docsearch-hit-highlight-color
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.DocSearch-SearchBar {
|
.DocSearch-SearchBar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.DocSearch-Button {
|
.DocSearch-Button {
|
||||||
|
|
@ -95,6 +94,10 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.DocSearch-Sidepanel .DocSearch-AskAiScreen-Query {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.DocSearch-SidepanelButton.inline {
|
.DocSearch-SidepanelButton.inline {
|
||||||
background-color: var(--ifm-background-color);
|
background-color: var(--ifm-background-color);
|
||||||
}
|
}
|
||||||
|
|
@ -105,15 +108,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* is aesthetically better to have no border radius */
|
/* is aesthetically better to have no border radius */
|
||||||
@media screen and (min-width: 769px) {
|
@media screen and (width >= 769px) {
|
||||||
.DocSearch-Sidepanel-Container.inline.side-right {
|
.DocSearch-Sidepanel-Container.inline.side-right {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.DocSearch-Sidepanel-Container.inline.side-left {
|
.DocSearch-Sidepanel-Container.inline.side-left {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.DocSearch-AskAiScreen-Query {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.DocSearch-Container {
|
.DocSearch-Container {
|
||||||
z-index: calc(var(--ifm-z-index-fixed) + 1);
|
z-index: calc(var(--ifm-z-index-fixed) + 1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
|
* in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { translate } from '@docusaurus/Translate';
|
||||||
|
import React, { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
import styles from '../styles.module.css';
|
||||||
|
import type { Refinements } from '../types';
|
||||||
|
|
||||||
|
type ActiveRefinementsProps = {
|
||||||
|
refinements: Refinements;
|
||||||
|
onRemove: (attribute: string, value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ActiveRefinements({
|
||||||
|
refinements,
|
||||||
|
onRemove,
|
||||||
|
}: ActiveRefinementsProps): ReactNode {
|
||||||
|
const entries = Object.entries(refinements).flatMap(([attribute, values]) =>
|
||||||
|
values.map((value) => ({ attribute, value }))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.activeRefinements}>
|
||||||
|
{entries.map(({ attribute, value }) => (
|
||||||
|
<button
|
||||||
|
key={`${attribute}:${value}`}
|
||||||
|
type="button"
|
||||||
|
className={styles.activeRefinement}
|
||||||
|
aria-label={translate(
|
||||||
|
{
|
||||||
|
id: 'theme.SearchPage.removeFilter',
|
||||||
|
message: 'Remove filter {value}',
|
||||||
|
description:
|
||||||
|
'The label for the button that removes an active filter',
|
||||||
|
},
|
||||||
|
{ value }
|
||||||
|
)}
|
||||||
|
onClick={() => onRemove(attribute, value)}
|
||||||
|
>
|
||||||
|
<span>{value}</span>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 20 20" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.8"
|
||||||
|
strokeLinecap="round"
|
||||||
|
d="M5 5l10 10M15 5 5 15"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
|
* in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
|
import { translate } from '@docusaurus/Translate';
|
||||||
|
import React, { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
import styles from '../styles.module.css';
|
||||||
|
|
||||||
|
function AlgoliaLogo(): ReactNode {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="80"
|
||||||
|
height="24"
|
||||||
|
aria-label="Algolia"
|
||||||
|
role="img"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 2196.2 500"
|
||||||
|
className={styles.algoliaLogo}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<style>{`.cls-1,.cls-2{fill:#003dff}.cls-2{fill-rule:evenodd}`}</style>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
className="cls-2"
|
||||||
|
d="M1070.38,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
className="cls-1"
|
||||||
|
x="1845.88"
|
||||||
|
y="104.73"
|
||||||
|
width="62.58"
|
||||||
|
height="277.9"
|
||||||
|
rx="5.9"
|
||||||
|
ry="5.9"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="cls-2"
|
||||||
|
d="M1851.78,71.38h50.77c3.26,0,5.9-2.64,5.9-5.9V5.9c0-3.62-3.24-6.39-6.82-5.83l-50.77,7.95c-2.87,.45-4.99,2.92-4.99,5.83v51.62c0,3.26,2.64,5.9,5.9,5.9Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="cls-2"
|
||||||
|
d="M1764.03,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="cls-2"
|
||||||
|
d="M1631.95,142.72c-11.14-12.25-24.83-21.65-40.78-28.31-15.92-6.53-33.26-9.85-52.07-9.85-18.78,0-36.15,3.17-51.92,9.85-15.59,6.66-29.29,16.05-40.76,28.31-11.47,12.23-20.38,26.87-26.76,44.03-6.38,17.17-9.24,37.37-9.24,58.36,0,20.99,3.19,36.87,9.55,54.21,6.38,17.32,15.14,32.11,26.45,44.36,11.29,12.23,24.83,21.62,40.6,28.46,15.77,6.83,40.12,10.33,52.4,10.48,12.25,0,36.78-3.82,52.7-10.48,15.92-6.68,29.46-16.23,40.78-28.46,11.29-12.25,20.05-27.04,26.25-44.36,6.22-17.34,9.24-33.22,9.24-54.21,0-20.99-3.34-41.19-10.03-58.36-6.38-17.17-15.14-31.8-26.43-44.03Zm-44.43,163.75c-11.47,15.75-27.56,23.7-48.09,23.7-20.55,0-36.63-7.8-48.1-23.7-11.47-15.75-17.21-34.01-17.21-61.2,0-26.89,5.59-49.14,17.06-64.87,11.45-15.75,27.54-23.52,48.07-23.52,20.55,0,36.63,7.78,48.09,23.52,11.47,15.57,17.36,37.98,17.36,64.87,0,27.19-5.72,45.3-17.19,61.2Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="cls-2"
|
||||||
|
d="M894.42,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="cls-2"
|
||||||
|
d="M2133.97,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="cls-2"
|
||||||
|
d="M1314.05,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-11.79,18.34-19.6,39.64-22.11,62.59-.58,5.3-.88,10.68-.88,16.14s.31,11.15,.93,16.59c4.28,38.09,23.14,71.61,50.66,94.52,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47h0c17.99,0,34.61-5.93,48.16-15.97,16.29-11.58,28.88-28.54,34.48-47.75v50.26h-.11v11.08c0,21.84-5.71,38.27-17.34,49.36-11.61,11.08-31.04,16.63-58.25,16.63-11.12,0-28.79-.59-46.6-2.41-2.83-.29-5.46,1.5-6.27,4.22l-12.78,43.11c-1.02,3.46,1.27,7.02,4.83,7.53,21.52,3.08,42.52,4.68,54.65,4.68,48.91,0,85.16-10.75,108.89-32.21,21.48-19.41,33.15-48.89,35.2-88.52V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,64.1s.65,139.13,0,143.36c-12.08,9.77-27.11,13.59-43.49,14.7-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-1.32,0-2.63-.03-3.94-.1-40.41-2.11-74.52-37.26-74.52-79.38,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="cls-1"
|
||||||
|
d="M249.83,0C113.3,0,2,110.09,.03,246.16c-2,138.19,110.12,252.7,248.33,253.5,42.68,.25,83.79-10.19,120.3-30.03,3.56-1.93,4.11-6.83,1.08-9.51l-23.38-20.72c-4.75-4.21-11.51-5.4-17.36-2.92-25.48,10.84-53.17,16.38-81.71,16.03-111.68-1.37-201.91-94.29-200.13-205.96,1.76-110.26,92-199.41,202.67-199.41h202.69V407.41l-115-102.18c-3.72-3.31-9.42-2.66-12.42,1.31-18.46,24.44-48.53,39.64-81.93,37.34-46.33-3.2-83.87-40.5-87.34-86.81-4.15-55.24,39.63-101.52,94-101.52,49.18,0,89.68,37.85,93.91,85.95,.38,4.28,2.31,8.27,5.52,11.12l29.95,26.55c3.4,3.01,8.79,1.17,9.63-3.3,2.16-11.55,2.92-23.58,2.07-35.92-4.82-70.34-61.8-126.93-132.17-131.26-80.68-4.97-148.13,58.14-150.27,137.25-2.09,77.1,61.08,143.56,138.19,145.26,32.19,.71,62.03-9.41,86.14-26.95l150.26,133.2c6.44,5.71,16.61,1.14,16.61-7.47V9.48C499.66,4.25,495.42,0,490.18,0H249.83Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PoweredByAlgolia(): ReactNode {
|
||||||
|
return (
|
||||||
|
<div className={styles.poweredBy}>
|
||||||
|
<span className={styles.poweredByLabel}>
|
||||||
|
{translate({
|
||||||
|
id: 'theme.SearchPage.algoliaLabel',
|
||||||
|
message: 'Powered by',
|
||||||
|
description: 'The text explain that the search powered by Algolia',
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<Link
|
||||||
|
to="https://www.algolia.com/"
|
||||||
|
className={styles.poweredByLink}
|
||||||
|
aria-label={translate({
|
||||||
|
id: 'theme.SearchPage.algoliaLabel',
|
||||||
|
message: 'Powered by Algolia',
|
||||||
|
description: 'The description label for Algolia mention',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<AlgoliaLogo />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
|
* in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
import styles from '../styles.module.css';
|
||||||
|
|
||||||
|
// `content` hits point at a paragraph within a page; every other type is a
|
||||||
|
// heading/section anchor.
|
||||||
|
function isContentType(type: string): boolean {
|
||||||
|
return type === 'content';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResultIcon({ type }: { type: string }): ReactNode {
|
||||||
|
if (isContentType(type)) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={styles.resultIcon}
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.6"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M4 4.5h12M4 8h12M4 11.5h8M4 15h6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={styles.resultIcon}
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.6"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M7.5 3.5 6 16.5M14 3.5l-1.5 13M3.5 7h13M3 13h13"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
|
* in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { translate } from '@docusaurus/Translate';
|
||||||
|
import React, { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
import styles from '../styles.module.css';
|
||||||
|
import type { FacetValueItem } from '../types';
|
||||||
|
|
||||||
|
type SearchEmptyStateProps = {
|
||||||
|
recentSearches: string[];
|
||||||
|
onSelectRecent: (query: string) => void;
|
||||||
|
onRemoveRecent: (query: string) => void;
|
||||||
|
onClearRecent: () => void;
|
||||||
|
topSections: FacetValueItem[];
|
||||||
|
onSelectSection: (section: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SearchEmptyState({
|
||||||
|
recentSearches,
|
||||||
|
onSelectRecent,
|
||||||
|
onRemoveRecent,
|
||||||
|
onClearRecent,
|
||||||
|
topSections,
|
||||||
|
onSelectSection,
|
||||||
|
}: SearchEmptyStateProps): ReactNode {
|
||||||
|
const hasContent = recentSearches.length > 0 || topSections.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.emptyState}>
|
||||||
|
{recentSearches.length > 0 && (
|
||||||
|
<section className={styles.emptySection}>
|
||||||
|
<div className={styles.emptySectionHeader}>
|
||||||
|
<h2 className={styles.emptySectionTitle}>
|
||||||
|
{translate({
|
||||||
|
id: 'theme.SearchPage.recentSearches',
|
||||||
|
message: 'Recent searches',
|
||||||
|
description: 'The heading for the list of recent searches',
|
||||||
|
})}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.emptySectionAction}
|
||||||
|
onClick={onClearRecent}
|
||||||
|
>
|
||||||
|
{translate({
|
||||||
|
id: 'theme.SearchPage.clearRecentSearches',
|
||||||
|
message: 'Clear',
|
||||||
|
description:
|
||||||
|
'The label for the button that clears recent searches',
|
||||||
|
})}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className={styles.recentList}>
|
||||||
|
{recentSearches.map((query) => (
|
||||||
|
<li key={query} className={styles.recentItem}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.recentButton}
|
||||||
|
onClick={() => onSelectRecent(query)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.6"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M10 5.5V10l3 2m4-2a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>{query}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.recentRemove}
|
||||||
|
aria-label={translate(
|
||||||
|
{
|
||||||
|
id: 'theme.SearchPage.removeRecentSearch',
|
||||||
|
message: 'Remove "{query}" from history',
|
||||||
|
description:
|
||||||
|
'The label for the button that removes a recent search',
|
||||||
|
},
|
||||||
|
{ query }
|
||||||
|
)}
|
||||||
|
onClick={() => onRemoveRecent(query)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.8"
|
||||||
|
strokeLinecap="round"
|
||||||
|
d="M5 5l10 10M15 5 5 15"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{topSections.length > 0 && (
|
||||||
|
<section className={styles.emptySection}>
|
||||||
|
<h2 className={styles.emptySectionTitle}>
|
||||||
|
{translate({
|
||||||
|
id: 'theme.SearchPage.browseSections',
|
||||||
|
message: 'Browse by section',
|
||||||
|
description:
|
||||||
|
'The heading for the list of documentation sections to browse',
|
||||||
|
})}
|
||||||
|
</h2>
|
||||||
|
<div className={styles.sectionChips}>
|
||||||
|
{topSections.map((section) => (
|
||||||
|
<button
|
||||||
|
key={section.name}
|
||||||
|
type="button"
|
||||||
|
className={styles.sectionChip}
|
||||||
|
onClick={() => onSelectSection(section.name)}
|
||||||
|
>
|
||||||
|
<span>{section.name}</span>
|
||||||
|
<span className={styles.sectionChipCount}>{section.count}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!hasContent && (
|
||||||
|
<p className={styles.emptyHint}>
|
||||||
|
{translate({
|
||||||
|
id: 'theme.SearchPage.emptyHint',
|
||||||
|
message: 'Start typing to search the documentation.',
|
||||||
|
description:
|
||||||
|
'The hint shown on the search page before the user types anything',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,212 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
|
* in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { translate } from '@docusaurus/Translate';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React, { useState, type ReactNode } from 'react';
|
||||||
|
|
||||||
|
import type { DocsSearchVersionsHelpers } from '../hooks/useDocsSearchVersions';
|
||||||
|
import styles from '../styles.module.css';
|
||||||
|
import type { FacetGroup, Refinements } from '../types';
|
||||||
|
|
||||||
|
type SearchFacetsProps = {
|
||||||
|
facets: FacetGroup[];
|
||||||
|
refinements: Refinements;
|
||||||
|
onToggle: (attribute: string, value: string) => void;
|
||||||
|
onClear: () => void;
|
||||||
|
hasActiveRefinements: boolean;
|
||||||
|
showVersionSelects: boolean;
|
||||||
|
versionHelpers: DocsSearchVersionsHelpers;
|
||||||
|
};
|
||||||
|
|
||||||
|
function VersionSelects({
|
||||||
|
versionHelpers,
|
||||||
|
}: {
|
||||||
|
versionHelpers: DocsSearchVersionsHelpers;
|
||||||
|
}): ReactNode {
|
||||||
|
const versionedPluginEntries = Object.entries(
|
||||||
|
versionHelpers.allDocsData
|
||||||
|
).filter(([, docsData]) => docsData.versions.length > 1);
|
||||||
|
|
||||||
|
if (versionedPluginEntries.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.facetGroup}>
|
||||||
|
<h3 className={styles.facetLabel}>
|
||||||
|
{translate({
|
||||||
|
id: 'theme.SearchPage.versionLabel',
|
||||||
|
message: 'Version',
|
||||||
|
description: 'The label above the documentation version selectors',
|
||||||
|
})}
|
||||||
|
</h3>
|
||||||
|
{versionedPluginEntries.map(([pluginId, docsData]) => {
|
||||||
|
const labelPrefix =
|
||||||
|
versionedPluginEntries.length > 1 ? `${pluginId}: ` : '';
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
key={pluginId}
|
||||||
|
defaultValue={versionHelpers.searchVersions[pluginId]}
|
||||||
|
className={styles.versionSelect}
|
||||||
|
onBlur={(event) =>
|
||||||
|
versionHelpers.setSearchVersion(pluginId, event.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{docsData.versions.map((version) => (
|
||||||
|
<option
|
||||||
|
key={version.name}
|
||||||
|
label={`${labelPrefix}${version.label}`}
|
||||||
|
value={version.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchFacets({
|
||||||
|
facets,
|
||||||
|
refinements,
|
||||||
|
onToggle,
|
||||||
|
onClear,
|
||||||
|
hasActiveRefinements,
|
||||||
|
showVersionSelects,
|
||||||
|
versionHelpers,
|
||||||
|
}: SearchFacetsProps): ReactNode {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const activeCount = Object.values(refinements).reduce(
|
||||||
|
(sum, values) => sum + values.length,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside
|
||||||
|
className={styles.sidebar}
|
||||||
|
aria-label={translate({
|
||||||
|
id: 'theme.SearchPage.filtersLabel',
|
||||||
|
message: 'Search filters',
|
||||||
|
description: 'The ARIA label for the search filters sidebar',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={clsx(styles.filtersToggle, open && styles.filtersToggleOpen)}
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-controls="search-page-filters-panel"
|
||||||
|
onClick={() => setOpen((current) => !current)}
|
||||||
|
>
|
||||||
|
<span className={styles.filtersToggleLabel}>
|
||||||
|
{translate({
|
||||||
|
id: 'theme.SearchPage.filtersToggle',
|
||||||
|
message: 'Filters',
|
||||||
|
description: 'The label for the mobile filters toggle button',
|
||||||
|
})}
|
||||||
|
{activeCount > 0 && (
|
||||||
|
<span className={styles.filtersToggleBadge}>{activeCount}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
className={styles.filtersToggleChevron}
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4 6l4 4 4-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="search-page-filters-panel"
|
||||||
|
className={clsx(styles.sidebarPanel, open && styles.sidebarPanelOpen)}
|
||||||
|
>
|
||||||
|
{hasActiveRefinements && (
|
||||||
|
<div className={styles.sidebarHeaderMobileClear}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.clearFacetsButton}
|
||||||
|
onClick={onClear}
|
||||||
|
>
|
||||||
|
{translate({
|
||||||
|
id: 'theme.SearchPage.clearFilters',
|
||||||
|
message: 'Clear all',
|
||||||
|
description:
|
||||||
|
'The label for the button that clears all active filters',
|
||||||
|
})}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.sidebarHeaderDesktop}>
|
||||||
|
<span className={styles.sidebarTitle}>
|
||||||
|
{translate({
|
||||||
|
id: 'theme.SearchPage.filtersTitle',
|
||||||
|
message: 'Filters',
|
||||||
|
description: 'The title of the search filters sidebar',
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
{hasActiveRefinements && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.clearFacetsButton}
|
||||||
|
onClick={onClear}
|
||||||
|
>
|
||||||
|
{translate({
|
||||||
|
id: 'theme.SearchPage.clearFilters',
|
||||||
|
message: 'Clear all',
|
||||||
|
description:
|
||||||
|
'The label for the button that clears all active filters',
|
||||||
|
})}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showVersionSelects && (
|
||||||
|
<VersionSelects versionHelpers={versionHelpers} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{facets.map((group) => (
|
||||||
|
<div key={group.attribute} className={styles.facetGroup}>
|
||||||
|
<h3 className={styles.facetLabel}>{group.label}</h3>
|
||||||
|
<ul className={styles.facetList}>
|
||||||
|
{group.items.map((value) => {
|
||||||
|
const isChecked =
|
||||||
|
refinements[group.attribute]?.includes(value.name) ??
|
||||||
|
value.isRefined;
|
||||||
|
return (
|
||||||
|
<li key={value.name}>
|
||||||
|
<label className={styles.facetItem}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className={styles.facetCheckbox}
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={() => onToggle(group.attribute, value.name)}
|
||||||
|
/>
|
||||||
|
<span className={styles.facetName}>{value.name}</span>
|
||||||
|
<span className={styles.facetCount}>{value.count}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
/* eslint-disable jsx-a11y/no-autofocus */
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
|
* in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { translate } from '@docusaurus/Translate';
|
||||||
|
import React, { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
import styles from '../styles.module.css';
|
||||||
|
|
||||||
|
type SearchInputProps = {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onClear: () => void;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SearchInput({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onClear,
|
||||||
|
autoFocus = false,
|
||||||
|
}: SearchInputProps): ReactNode {
|
||||||
|
return (
|
||||||
|
<div className={styles.searchBox}>
|
||||||
|
<svg
|
||||||
|
className={styles.searchBoxIcon}
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.8"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M14.386 14.386 18 18m-2-8a6 6 0 1 1-12 0 6 6 0 0 1 12 0Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<input
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
type="search"
|
||||||
|
name="q"
|
||||||
|
className={styles.searchInput}
|
||||||
|
placeholder={translate({
|
||||||
|
id: 'theme.SearchPage.inputPlaceholder',
|
||||||
|
message: 'Type your search here',
|
||||||
|
description: 'The placeholder for search page input',
|
||||||
|
})}
|
||||||
|
aria-label={translate({
|
||||||
|
id: 'theme.SearchPage.inputLabel',
|
||||||
|
message: 'Search',
|
||||||
|
description: 'The ARIA label for search page input',
|
||||||
|
})}
|
||||||
|
value={value}
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{value.length > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.searchBoxClear}
|
||||||
|
aria-label={translate({
|
||||||
|
id: 'theme.SearchPage.clearInputLabel',
|
||||||
|
message: 'Clear the search query',
|
||||||
|
description:
|
||||||
|
'The label for the button that clears the search query',
|
||||||
|
})}
|
||||||
|
onClick={onClear}
|
||||||
|
>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 20 20" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.8"
|
||||||
|
strokeLinecap="round"
|
||||||
|
d="M5 5l10 10M15 5 5 15"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
|
* in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
|
import React, { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
import styles from '../styles.module.css';
|
||||||
|
import type { SearchResultItem as SearchResultItemType } from '../types';
|
||||||
|
|
||||||
|
import { ResultIcon } from './ResultIcon';
|
||||||
|
|
||||||
|
type SearchResultItemProps = {
|
||||||
|
item: SearchResultItemType;
|
||||||
|
position: number;
|
||||||
|
onSelect: (item: SearchResultItemType, position: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SearchResultItem({
|
||||||
|
item,
|
||||||
|
position,
|
||||||
|
onSelect,
|
||||||
|
}: SearchResultItemProps): ReactNode {
|
||||||
|
return (
|
||||||
|
<li className={styles.resultItem}>
|
||||||
|
<Link
|
||||||
|
to={item.url}
|
||||||
|
className={styles.resultLink}
|
||||||
|
onClick={() => onSelect(item, position)}
|
||||||
|
>
|
||||||
|
<span className={styles.resultIconWrapper}>
|
||||||
|
<ResultIcon type={item.type} />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className={styles.resultContent}>
|
||||||
|
{item.breadcrumbs.length > 0 && (
|
||||||
|
<span className={styles.resultBreadcrumbs}>
|
||||||
|
{item.breadcrumbs.map((crumb, index) => (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
{index > 0 && (
|
||||||
|
<span
|
||||||
|
className={styles.resultBreadcrumbSeparator}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{/* Highlighted HTML comes from Algolia. */}
|
||||||
|
{/* eslint-disable-next-line react/no-danger */}
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: crumb }} />
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Highlighted HTML comes from Algolia. */}
|
||||||
|
{/* eslint-disable-next-line react/no-danger */}
|
||||||
|
<span
|
||||||
|
className={styles.resultTitle}
|
||||||
|
dangerouslySetInnerHTML={{ __html: item.title }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{item.summary && (
|
||||||
|
// eslint-disable-next-line react/no-danger
|
||||||
|
<span
|
||||||
|
className={styles.resultSummary}
|
||||||
|
dangerouslySetInnerHTML={{ __html: item.summary }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
className={styles.resultArrow}
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.8"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M7.5 4.5 13 10l-5.5 5.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
|
* in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
import styles from '../styles.module.css';
|
||||||
|
import type { SearchResultItem as SearchResultItemType } from '../types';
|
||||||
|
|
||||||
|
import { SearchResultItem } from './SearchResultItem';
|
||||||
|
|
||||||
|
type SearchResultsProps = {
|
||||||
|
items: SearchResultItemType[];
|
||||||
|
onSelect: (item: SearchResultItemType, position: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SearchResults({
|
||||||
|
items,
|
||||||
|
onSelect,
|
||||||
|
}: SearchResultsProps): ReactNode {
|
||||||
|
return (
|
||||||
|
<ol className={styles.resultList}>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<SearchResultItem
|
||||||
|
key={item.objectID}
|
||||||
|
item={item}
|
||||||
|
position={index + 1}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
|
* in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { translate } from '@docusaurus/Translate';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React, { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
import styles from '../styles.module.css';
|
||||||
|
|
||||||
|
export function SearchResultsSkeleton({
|
||||||
|
count = 5,
|
||||||
|
}: {
|
||||||
|
count?: number;
|
||||||
|
}): ReactNode {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.skeletonList}
|
||||||
|
aria-busy="true"
|
||||||
|
aria-label={translate({
|
||||||
|
id: 'theme.SearchPage.loadingResults',
|
||||||
|
message: 'Loading search results',
|
||||||
|
description:
|
||||||
|
'The ARIA label announced while search results are loading',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{Array.from({ length: count }).map((_, index) => (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
<div key={index} className={styles.skeletonItem}>
|
||||||
|
<div className={styles.skeletonIcon} />
|
||||||
|
<div className={styles.skeletonBody}>
|
||||||
|
<div
|
||||||
|
className={clsx(styles.skeletonLine, styles.skeletonLineShort)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={clsx(styles.skeletonLine, styles.skeletonLineTitle)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={clsx(styles.skeletonLine, styles.skeletonLineWide)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
|
* in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { SearchPageFacetConfig } from './types';
|
||||||
|
|
||||||
|
export const HITS_PER_PAGE = 15;
|
||||||
|
|
||||||
|
// `content:<words>` snippet, keeps result cards scannable
|
||||||
|
export const SNIPPET_ATTRIBUTES = ['content:30'];
|
||||||
|
export const SNIPPET_ELLIPSIS_TEXT = '…';
|
||||||
|
|
||||||
|
export const SEARCH_DEBOUNCE_MS = 300;
|
||||||
|
|
||||||
|
export const RECENT_SEARCHES_KEY = 'docsearch:search-page:recent';
|
||||||
|
export const RECENT_SEARCHES_LIMIT = 6;
|
||||||
|
|
||||||
|
// Facets rendered in the sidebar when the user doesn't configure their own.
|
||||||
|
// `hierarchy.lvl0` is the top-level documentation section, which is the most
|
||||||
|
// useful, least noisy facet for a docs search page.
|
||||||
|
export const DEFAULT_FACETS: SearchPageFacetConfig[] = [
|
||||||
|
{ attribute: 'hierarchy.lvl0', label: 'Section' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FACET_VALUES_LIMIT = 12;
|
||||||
|
|
||||||
|
// Number of "Browse by section" chips shown on the empty state.
|
||||||
|
export const TOP_SECTIONS_LIMIT = 8;
|
||||||
|
export const TOP_SECTIONS_FACET = 'hierarchy.lvl0';
|
||||||
|
|
||||||
|
export const FACET_SORT_BY = ['isRefined:desc', 'count:desc', 'name:asc'];
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
|
* in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useAllDocsData } from '@docusaurus/plugin-content-docs/client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export type DocsSearchVersionsHelpers = {
|
||||||
|
allDocsData: ReturnType<typeof useAllDocsData>;
|
||||||
|
versioningEnabled: boolean;
|
||||||
|
searchVersions: { [pluginId: string]: string };
|
||||||
|
setSearchVersion: (pluginId: string, searchVersion: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useDocsSearchVersions(): DocsSearchVersionsHelpers {
|
||||||
|
const allDocsData = useAllDocsData();
|
||||||
|
|
||||||
|
// State of the version select menus / algolia facet filters
|
||||||
|
// docsPluginId -> versionName map
|
||||||
|
const [searchVersions, setSearchVersions] = useState<{
|
||||||
|
[pluginId: string]: string;
|
||||||
|
}>(() =>
|
||||||
|
Object.entries(allDocsData).reduce(
|
||||||
|
(acc, [pluginId, pluginData]) => ({
|
||||||
|
...acc,
|
||||||
|
[pluginId]: pluginData.versions[0]!.name,
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const setSearchVersion = (pluginId: string, searchVersion: string): void =>
|
||||||
|
setSearchVersions((s) => ({ ...s, [pluginId]: searchVersion }));
|
||||||
|
|
||||||
|
const versioningEnabled = Object.values(allDocsData).some(
|
||||||
|
(docsData) => docsData.versions.length > 1
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
allDocsData,
|
||||||
|
versioningEnabled,
|
||||||
|
searchVersions,
|
||||||
|
setSearchVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
|
* in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { RECENT_SEARCHES_KEY, RECENT_SEARCHES_LIMIT } from '../constants';
|
||||||
|
|
||||||
|
function readRecentSearches(): string[] {
|
||||||
|
if (!ExecutionEnvironment.canUseDOM) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem(RECENT_SEARCHES_KEY);
|
||||||
|
if (!raw) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return Array.isArray(parsed)
|
||||||
|
? parsed.filter((item): item is string => typeof item === 'string')
|
||||||
|
: [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeRecentSearches(searches: string[]): void {
|
||||||
|
if (!ExecutionEnvironment.canUseDOM) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(searches));
|
||||||
|
} catch {
|
||||||
|
// Ignore write failures (private mode, quota, etc.): recent searches are
|
||||||
|
// a non-critical enhancement.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type UseRecentSearches = {
|
||||||
|
recentSearches: string[];
|
||||||
|
addRecentSearch: (query: string) => void;
|
||||||
|
removeRecentSearch: (query: string) => void;
|
||||||
|
clearRecentSearches: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useRecentSearches(): UseRecentSearches {
|
||||||
|
// Start empty so the server and first client render match, then hydrate from
|
||||||
|
// localStorage after mount to avoid hydration mismatches.
|
||||||
|
const [recentSearches, setRecentSearches] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRecentSearches(readRecentSearches());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const persist = useCallback((next: string[]) => {
|
||||||
|
setRecentSearches(next);
|
||||||
|
writeRecentSearches(next);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addRecentSearch = useCallback((query: string) => {
|
||||||
|
const trimmed = query.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRecentSearches((prev) => {
|
||||||
|
const next = [trimmed, ...prev.filter((item) => item !== trimmed)].slice(
|
||||||
|
0,
|
||||||
|
RECENT_SEARCHES_LIMIT
|
||||||
|
);
|
||||||
|
writeRecentSearches(next);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeRecentSearch = useCallback(
|
||||||
|
(query: string) => {
|
||||||
|
persist(recentSearches.filter((item) => item !== query));
|
||||||
|
},
|
||||||
|
[persist, recentSearches]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearRecentSearches = useCallback(() => {
|
||||||
|
persist([]);
|
||||||
|
}, [persist]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
recentSearches,
|
||||||
|
addRecentSearch,
|
||||||
|
removeRecentSearch,
|
||||||
|
clearRecentSearches,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,517 @@
|
||||||
|
/* eslint-disable import/dynamic-import-chunkname */
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
|
* in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||||
|
import { useHistory } from '@docusaurus/router';
|
||||||
|
import {
|
||||||
|
useEvent,
|
||||||
|
useHistorySelector,
|
||||||
|
useSearchQueryString,
|
||||||
|
} from '@docusaurus/theme-common';
|
||||||
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
|
import type algoliaSearchHelper from 'algoliasearch-helper';
|
||||||
|
import createAlgoliaSearchHelper from 'algoliasearch-helper';
|
||||||
|
import { liteClient } from 'algoliasearch/lite';
|
||||||
|
import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react';
|
||||||
|
import type { InsightsClient } from 'search-insights';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useAlgoliaThemeConfig,
|
||||||
|
useSearchResultUrlProcessor,
|
||||||
|
} from '../../../client';
|
||||||
|
import {
|
||||||
|
DEFAULT_FACETS,
|
||||||
|
FACET_SORT_BY,
|
||||||
|
FACET_VALUES_LIMIT,
|
||||||
|
HITS_PER_PAGE,
|
||||||
|
SEARCH_DEBOUNCE_MS,
|
||||||
|
SNIPPET_ATTRIBUTES,
|
||||||
|
SNIPPET_ELLIPSIS_TEXT,
|
||||||
|
TOP_SECTIONS_FACET,
|
||||||
|
} from '../constants';
|
||||||
|
import type {
|
||||||
|
FacetGroup,
|
||||||
|
FacetValueItem,
|
||||||
|
Refinements,
|
||||||
|
SearchPageFacetConfig,
|
||||||
|
SearchResultItem,
|
||||||
|
} from '../types';
|
||||||
|
import { getIndexName, getIndexSearchParameters } from '../utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useDocsSearchVersions,
|
||||||
|
type DocsSearchVersionsHelpers,
|
||||||
|
} from './useDocsSearchVersions';
|
||||||
|
|
||||||
|
type AlgoliaDocHit = {
|
||||||
|
objectID: string;
|
||||||
|
url: string;
|
||||||
|
type?: string;
|
||||||
|
_highlightResult?: { hierarchy?: { [key: string]: { value: string } } };
|
||||||
|
_snippetResult?: { content?: { value: string } };
|
||||||
|
};
|
||||||
|
|
||||||
|
type SearchResultUpdate = {
|
||||||
|
items: SearchResultItem[];
|
||||||
|
query: string;
|
||||||
|
queryID: string | undefined;
|
||||||
|
totalResults: number;
|
||||||
|
totalPages: number;
|
||||||
|
lastPage: number;
|
||||||
|
hasMore: boolean;
|
||||||
|
facets: FacetGroup[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type SearchState = {
|
||||||
|
items: SearchResultItem[];
|
||||||
|
query: string | null;
|
||||||
|
queryID: string | undefined;
|
||||||
|
totalResults: number | null;
|
||||||
|
totalPages: number | null;
|
||||||
|
lastPage: number | null;
|
||||||
|
hasMore: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
loadingMore: boolean;
|
||||||
|
error: boolean;
|
||||||
|
facets: FacetGroup[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type SearchAction =
|
||||||
|
| { type: 'advance' }
|
||||||
|
| { type: 'error' }
|
||||||
|
| { type: 'loading' }
|
||||||
|
| { type: 'reset' }
|
||||||
|
| { type: 'update'; value: SearchResultUpdate };
|
||||||
|
|
||||||
|
const initialSearchState: SearchState = {
|
||||||
|
items: [],
|
||||||
|
query: null,
|
||||||
|
queryID: undefined,
|
||||||
|
totalResults: null,
|
||||||
|
totalPages: null,
|
||||||
|
lastPage: null,
|
||||||
|
hasMore: false,
|
||||||
|
loading: false,
|
||||||
|
loadingMore: false,
|
||||||
|
error: false,
|
||||||
|
facets: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
function searchReducer(
|
||||||
|
prevState: SearchState,
|
||||||
|
action: SearchAction
|
||||||
|
): SearchState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'reset': {
|
||||||
|
return initialSearchState;
|
||||||
|
}
|
||||||
|
case 'loading': {
|
||||||
|
return { ...prevState, loading: true, error: false };
|
||||||
|
}
|
||||||
|
case 'error': {
|
||||||
|
return { ...prevState, loading: false, loadingMore: false, error: true };
|
||||||
|
}
|
||||||
|
case 'advance': {
|
||||||
|
const hasMore =
|
||||||
|
(prevState.totalPages ?? 0) > (prevState.lastPage ?? 0) + 1;
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
lastPage: hasMore ? (prevState.lastPage ?? 0) + 1 : prevState.lastPage,
|
||||||
|
hasMore,
|
||||||
|
loadingMore: hasMore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const { value } = action;
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
items:
|
||||||
|
value.lastPage === 0
|
||||||
|
? value.items
|
||||||
|
: prevState.items.concat(value.items),
|
||||||
|
query: value.query,
|
||||||
|
queryID: value.queryID,
|
||||||
|
totalResults: value.totalResults,
|
||||||
|
totalPages: value.totalPages,
|
||||||
|
lastPage: value.lastPage,
|
||||||
|
hasMore: value.hasMore,
|
||||||
|
facets: value.facets,
|
||||||
|
loading: false,
|
||||||
|
loadingMore: false,
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const exhaustiveCheck: never = action;
|
||||||
|
return exhaustiveCheck;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocSearch-scraped records use a legacy highlight tag. Map it onto a class we
|
||||||
|
// can style on the search page.
|
||||||
|
function sanitizeHighlight(value: string): string {
|
||||||
|
return value.replace(
|
||||||
|
/algolia-docsearch-suggestion--highlight/g,
|
||||||
|
'search-result-match'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapHitToResultItem(
|
||||||
|
hit: AlgoliaDocHit,
|
||||||
|
processUrl: (url: string) => string
|
||||||
|
): SearchResultItem {
|
||||||
|
const hierarchy = hit._highlightResult?.hierarchy ?? {};
|
||||||
|
const titles = Object.keys(hierarchy).map((key) =>
|
||||||
|
sanitizeHighlight(hierarchy[key]!.value)
|
||||||
|
);
|
||||||
|
const snippet = hit._snippetResult?.content?.value;
|
||||||
|
|
||||||
|
return {
|
||||||
|
objectID: hit.objectID,
|
||||||
|
title: titles.pop() ?? '',
|
||||||
|
url: processUrl(hit.url),
|
||||||
|
summary: snippet ? sanitizeHighlight(snippet) : '',
|
||||||
|
breadcrumbs: titles,
|
||||||
|
type: hit.type ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toFacetValues(
|
||||||
|
raw: ReturnType<algoliaSearchHelper.SearchResults['getFacetValues']>
|
||||||
|
): FacetValueItem[] {
|
||||||
|
if (!Array.isArray(raw)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw.slice(0, FACET_VALUES_LIMIT).map((value) => ({
|
||||||
|
name: value.name,
|
||||||
|
count: value.count,
|
||||||
|
isRefined: value.isRefined,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseSearchPage = {
|
||||||
|
searchQuery: string;
|
||||||
|
setSearchQuery: (query: string) => void;
|
||||||
|
items: SearchResultItem[];
|
||||||
|
totalResults: number | null;
|
||||||
|
loading: boolean;
|
||||||
|
loadingMore: boolean;
|
||||||
|
error: boolean;
|
||||||
|
hasMore: boolean;
|
||||||
|
facets: FacetGroup[];
|
||||||
|
refinements: Refinements;
|
||||||
|
toggleRefinement: (attribute: string, value: string) => void;
|
||||||
|
clearRefinements: () => void;
|
||||||
|
hasActiveRefinements: boolean;
|
||||||
|
loadMore: () => void;
|
||||||
|
sendResultClick: (item: SearchResultItem, position: number) => void;
|
||||||
|
versionHelpers: DocsSearchVersionsHelpers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useSearchPage(): UseSearchPage {
|
||||||
|
const {
|
||||||
|
i18n: { currentLocale },
|
||||||
|
} = useDocusaurusContext();
|
||||||
|
const { appId, apiKey, indices, contextualSearch, insights, searchPage } =
|
||||||
|
useAlgoliaThemeConfig();
|
||||||
|
const insightsEnabled = Boolean(insights);
|
||||||
|
|
||||||
|
const searchIndex = indices[0]!;
|
||||||
|
const indexName = getIndexName(searchIndex);
|
||||||
|
const indexSearchParameters = useMemo(
|
||||||
|
() => getIndexSearchParameters(searchIndex),
|
||||||
|
[searchIndex]
|
||||||
|
);
|
||||||
|
const processSearchResultUrl = useSearchResultUrlProcessor();
|
||||||
|
|
||||||
|
const facetConfig = useMemo<SearchPageFacetConfig[]>(() => {
|
||||||
|
const configured = searchPage === false ? undefined : searchPage.facets;
|
||||||
|
return configured && configured.length > 0 ? configured : DEFAULT_FACETS;
|
||||||
|
}, [searchPage]);
|
||||||
|
const facetAttributes = useMemo(
|
||||||
|
() => facetConfig.map((facet) => facet.attribute),
|
||||||
|
[facetConfig]
|
||||||
|
);
|
||||||
|
// Attributes we can refine on via the URL. Always include the "browse by
|
||||||
|
// section" facet so empty-state section chips work even if the user
|
||||||
|
// customized `facets`.
|
||||||
|
const refinableAttributes = useMemo(
|
||||||
|
() => Array.from(new Set([...facetAttributes, TOP_SECTIONS_FACET])),
|
||||||
|
[facetAttributes]
|
||||||
|
);
|
||||||
|
|
||||||
|
const versionHelpers = useDocsSearchVersions();
|
||||||
|
const [searchQuery, setSearchQuery] = useSearchQueryString();
|
||||||
|
|
||||||
|
// Facet refinements live in the URL so filtered searches are shareable and
|
||||||
|
// work with browser back/forward.
|
||||||
|
const history = useHistory();
|
||||||
|
const refinementsJson = useHistorySelector((historyState) => {
|
||||||
|
const params = new URLSearchParams(historyState.location.search);
|
||||||
|
const result: Refinements = {};
|
||||||
|
refinableAttributes.forEach((attribute) => {
|
||||||
|
const values = params.getAll(attribute);
|
||||||
|
if (values.length > 0) {
|
||||||
|
result[attribute] = values;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return JSON.stringify(result);
|
||||||
|
});
|
||||||
|
const refinements = useMemo(
|
||||||
|
() => JSON.parse(refinementsJson) as Refinements,
|
||||||
|
[refinementsJson]
|
||||||
|
);
|
||||||
|
const hasActiveRefinements = Object.keys(refinements).length > 0;
|
||||||
|
|
||||||
|
const setAttributeValues = useEvent((attribute: string, values: string[]) => {
|
||||||
|
const params = new URLSearchParams(history.location.search);
|
||||||
|
params.delete(attribute);
|
||||||
|
values.forEach((value) => params.append(attribute, value));
|
||||||
|
history.replace({ search: params.toString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleRefinement = useEvent((attribute: string, value: string) => {
|
||||||
|
const current = refinements[attribute] ?? [];
|
||||||
|
const next = current.includes(value)
|
||||||
|
? current.filter((item) => item !== value)
|
||||||
|
: [...current, value];
|
||||||
|
setAttributeValues(attribute, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
const clearRefinements = useEvent(() => {
|
||||||
|
const params = new URLSearchParams(history.location.search);
|
||||||
|
refinableAttributes.forEach((attribute) => params.delete(attribute));
|
||||||
|
history.replace({ search: params.toString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
const [state, dispatch] = useReducer(searchReducer, initialSearchState);
|
||||||
|
|
||||||
|
// Create the Algolia client + helper once, rather than on every render.
|
||||||
|
const helper = useMemo(() => {
|
||||||
|
const client = liteClient(appId, apiKey);
|
||||||
|
const disjunctiveFacets = Array.from(
|
||||||
|
new Set([
|
||||||
|
...(contextualSearch ? ['language', 'docusaurus_tag'] : []),
|
||||||
|
...refinableAttributes,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
return createAlgoliaSearchHelper(client, indexName, {
|
||||||
|
...indexSearchParameters,
|
||||||
|
hitsPerPage: HITS_PER_PAGE,
|
||||||
|
advancedSyntax: true,
|
||||||
|
attributesToSnippet: SNIPPET_ATTRIBUTES,
|
||||||
|
snippetEllipsisText: SNIPPET_ELLIPSIS_TEXT,
|
||||||
|
disjunctiveFacets,
|
||||||
|
...(insightsEnabled ? { clickAnalytics: true } : {}),
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
appId,
|
||||||
|
apiKey,
|
||||||
|
indexName,
|
||||||
|
contextualSearch,
|
||||||
|
insightsEnabled,
|
||||||
|
indexSearchParameters,
|
||||||
|
refinableAttributes,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResult = ({
|
||||||
|
results,
|
||||||
|
}: {
|
||||||
|
results: algoliaSearchHelper.SearchResults;
|
||||||
|
}): void => {
|
||||||
|
const { query, hits, page, nbHits, nbPages, queryID } = results;
|
||||||
|
|
||||||
|
// Ignore stale responses for a previous query.
|
||||||
|
if (query !== searchQuery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An empty query with active refinements is a valid "browse by facet"
|
||||||
|
// search; only reset when there is genuinely nothing to search.
|
||||||
|
if (!Array.isArray(hits) || (query === '' && !hasActiveRefinements)) {
|
||||||
|
dispatch({ type: 'reset' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = (hits as AlgoliaDocHit[]).map((hit) =>
|
||||||
|
mapHitToResultItem(hit, processSearchResultUrl)
|
||||||
|
);
|
||||||
|
const facets = facetConfig
|
||||||
|
.map((facet) => ({
|
||||||
|
attribute: facet.attribute,
|
||||||
|
label: facet.label ?? facet.attribute,
|
||||||
|
items: toFacetValues(
|
||||||
|
results.getFacetValues(facet.attribute, { sortBy: FACET_SORT_BY })
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.filter((group) => group.items.length > 0);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'update',
|
||||||
|
value: {
|
||||||
|
items,
|
||||||
|
query,
|
||||||
|
queryID,
|
||||||
|
totalResults: nbHits,
|
||||||
|
totalPages: nbPages,
|
||||||
|
lastPage: page,
|
||||||
|
hasMore: nbPages > page + 1,
|
||||||
|
facets,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleError = (): void => {
|
||||||
|
dispatch({ type: 'error' });
|
||||||
|
};
|
||||||
|
|
||||||
|
helper.on('result', handleResult);
|
||||||
|
helper.on('error', handleError);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
helper.removeListener('result', handleResult);
|
||||||
|
helper.removeListener('error', handleError);
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
helper,
|
||||||
|
searchQuery,
|
||||||
|
hasActiveRefinements,
|
||||||
|
facetConfig,
|
||||||
|
processSearchResultUrl,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const makeSearch = useEvent((page: number = 0) => {
|
||||||
|
// Rebuild refinements from scratch each search, since the helper is stable.
|
||||||
|
helper.clearRefinements();
|
||||||
|
|
||||||
|
if (contextualSearch) {
|
||||||
|
helper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default');
|
||||||
|
helper.addDisjunctiveFacetRefinement('language', currentLocale);
|
||||||
|
|
||||||
|
Object.entries(versionHelpers.searchVersions).forEach(
|
||||||
|
([pluginId, searchVersion]) => {
|
||||||
|
helper.addDisjunctiveFacetRefinement(
|
||||||
|
'docusaurus_tag',
|
||||||
|
`docs-${pluginId}-${searchVersion}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(refinements).forEach(([attribute, values]) => {
|
||||||
|
values.forEach((value) =>
|
||||||
|
helper.addDisjunctiveFacetRefinement(attribute, value)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
helper.setQuery(searchQuery).setPage(page).search();
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch({ type: 'reset' });
|
||||||
|
|
||||||
|
// Search when there is a query, or when browsing by facet with no query.
|
||||||
|
if (!searchQuery && !hasActiveRefinements) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({ type: 'loading' });
|
||||||
|
|
||||||
|
const searchTimeoutId = setTimeout(() => {
|
||||||
|
makeSearch();
|
||||||
|
}, SEARCH_DEBOUNCE_MS);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(searchTimeoutId);
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
searchQuery,
|
||||||
|
hasActiveRefinements,
|
||||||
|
refinements,
|
||||||
|
versionHelpers.searchVersions,
|
||||||
|
makeSearch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!state.lastPage || state.lastPage === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeSearch(state.lastPage);
|
||||||
|
}, [makeSearch, state.lastPage]);
|
||||||
|
|
||||||
|
const loadMore = useCallback(() => {
|
||||||
|
dispatch({ type: 'advance' });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Lazily load Algolia Insights only when analytics is enabled, mirroring the
|
||||||
|
// DocSearch modal (which also gates click analytics behind `insights`).
|
||||||
|
const insightsRef = useRef<InsightsClient | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!insightsEnabled || !ExecutionEnvironment.canUseDOM) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
import('search-insights')
|
||||||
|
.then(({ default: aa }) => {
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
aa('init', { appId, apiKey });
|
||||||
|
insightsRef.current = aa;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Analytics is a non-critical enhancement; ignore load failures.
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [insightsEnabled, appId, apiKey]);
|
||||||
|
|
||||||
|
const sendResultClick = useEvent(
|
||||||
|
(item: SearchResultItem, position: number) => {
|
||||||
|
const aa = insightsRef.current;
|
||||||
|
if (!insightsEnabled || !aa || !state.queryID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
aa('clickedObjectIDsAfterSearch', {
|
||||||
|
eventName: 'Item Selected',
|
||||||
|
index: indexName,
|
||||||
|
queryID: state.queryID,
|
||||||
|
objectIDs: [item.objectID],
|
||||||
|
positions: [position],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchQuery,
|
||||||
|
setSearchQuery,
|
||||||
|
items: state.items,
|
||||||
|
totalResults: state.totalResults,
|
||||||
|
loading: state.loading,
|
||||||
|
loadingMore: state.loadingMore,
|
||||||
|
error: state.error,
|
||||||
|
hasMore: state.hasMore,
|
||||||
|
facets: state.facets,
|
||||||
|
refinements,
|
||||||
|
toggleRefinement,
|
||||||
|
clearRefinements,
|
||||||
|
hasActiveRefinements,
|
||||||
|
loadMore,
|
||||||
|
sendResultClick,
|
||||||
|
versionHelpers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
|
* in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||||
|
import { liteClient } from 'algoliasearch/lite';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useAlgoliaContextualFacetFiltersIfEnabled,
|
||||||
|
useAlgoliaThemeConfig,
|
||||||
|
} from '../../../client';
|
||||||
|
import { TOP_SECTIONS_FACET, TOP_SECTIONS_LIMIT } from '../constants';
|
||||||
|
import type { FacetValueItem } from '../types';
|
||||||
|
import { getIndexName } from '../utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the most common top-level sections (`hierarchy.lvl0`) so the empty
|
||||||
|
* state can offer "Browse by section" shortcuts. Runs a single facet-only query
|
||||||
|
* (no hits) on mount.
|
||||||
|
*/
|
||||||
|
export function useTopSections(): FacetValueItem[] {
|
||||||
|
const { appId, apiKey, indices } = useAlgoliaThemeConfig();
|
||||||
|
const facetFilters = useAlgoliaContextualFacetFiltersIfEnabled();
|
||||||
|
const searchIndex = indices[0]!;
|
||||||
|
const indexName = getIndexName(searchIndex);
|
||||||
|
|
||||||
|
const facetFiltersKey = useMemo(
|
||||||
|
() => JSON.stringify(facetFilters ?? null),
|
||||||
|
[facetFilters]
|
||||||
|
);
|
||||||
|
const [sections, setSections] = useState<FacetValueItem[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ExecutionEnvironment.canUseDOM) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
const client = liteClient(appId, apiKey);
|
||||||
|
const parsedFacetFilters = JSON.parse(facetFiltersKey);
|
||||||
|
|
||||||
|
client
|
||||||
|
.search({
|
||||||
|
requests: [
|
||||||
|
{
|
||||||
|
indexName,
|
||||||
|
query: '',
|
||||||
|
hitsPerPage: 0,
|
||||||
|
facets: [TOP_SECTIONS_FACET],
|
||||||
|
maxValuesPerFacet: TOP_SECTIONS_LIMIT,
|
||||||
|
...(parsedFacetFilters ? { facetFilters: parsedFacetFilters } : {}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const firstResult = response.results[0] as {
|
||||||
|
facets?: { [facet: string]: { [value: string]: number } };
|
||||||
|
};
|
||||||
|
const values = firstResult?.facets?.[TOP_SECTIONS_FACET] ?? {};
|
||||||
|
const items = Object.entries(values)
|
||||||
|
.map(([name, count]) => ({ name, count, isRefined: false }))
|
||||||
|
.sort((a, b) => b.count - a.count)
|
||||||
|
.slice(0, TOP_SECTIONS_LIMIT);
|
||||||
|
setSections(items);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Browse shortcuts are a non-critical enhancement; ignore failures.
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [appId, apiKey, indexName, facetFiltersKey]);
|
||||||
|
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
@ -1,32 +1,36 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable jsx-a11y/no-autofocus */
|
|
||||||
|
|
||||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
|
||||||
import Head from '@docusaurus/Head';
|
import Head from '@docusaurus/Head';
|
||||||
import Link from '@docusaurus/Link';
|
import { HtmlClassNameProvider, usePluralForm } from '@docusaurus/theme-common';
|
||||||
import { useAllDocsData } from '@docusaurus/plugin-content-docs/client';
|
|
||||||
import { HtmlClassNameProvider, useEvent, usePluralForm, useSearchQueryString } from '@docusaurus/theme-common';
|
|
||||||
import Translate, { translate } from '@docusaurus/Translate';
|
import Translate, { translate } from '@docusaurus/Translate';
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
||||||
import Heading from '@theme/Heading';
|
import Heading from '@theme/Heading';
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import { liteClient } from 'algoliasearch/lite';
|
|
||||||
import algoliaSearchHelper from 'algoliasearch-helper';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React, { type ReactNode, useEffect, useReducer, useRef, useState } from 'react';
|
import React, { type ReactNode } from 'react';
|
||||||
|
|
||||||
import { useAlgoliaThemeConfig, useSearchResultUrlProcessor } from '../../client';
|
import { useAlgoliaThemeConfig } from '../../client';
|
||||||
|
|
||||||
|
import { ActiveRefinements } from './components/ActiveRefinements';
|
||||||
|
import { PoweredByAlgolia } from './components/PoweredByAlgolia';
|
||||||
|
import { SearchEmptyState } from './components/SearchEmptyState';
|
||||||
|
import { SearchFacets } from './components/SearchFacets';
|
||||||
|
import { SearchInput } from './components/SearchInput';
|
||||||
|
import { SearchResults } from './components/SearchResults';
|
||||||
|
import { SearchResultsSkeleton } from './components/SearchResultsSkeleton';
|
||||||
|
import { TOP_SECTIONS_FACET } from './constants';
|
||||||
|
import { useRecentSearches } from './hooks/useRecentSearches';
|
||||||
|
import { useSearchPage } from './hooks/useSearchPage';
|
||||||
|
import { useTopSections } from './hooks/useTopSections';
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
import type { SearchResultItem } from './types';
|
||||||
|
|
||||||
// Very simple pluralization: probably good enough for now
|
// Very simple pluralization: probably good enough for now
|
||||||
function useDocumentsFoundPlural() {
|
function useDocumentsFoundPlural(): (count: number) => string {
|
||||||
const { selectMessage } = usePluralForm();
|
const { selectMessage } = usePluralForm();
|
||||||
return (count: number) =>
|
return (count: number) =>
|
||||||
selectMessage(
|
selectMessage(
|
||||||
|
|
@ -38,146 +42,11 @@ function useDocumentsFoundPlural() {
|
||||||
'Pluralized label for "{count} documents found". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',
|
'Pluralized label for "{count} documents found". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',
|
||||||
message: 'One document found|{count} documents found',
|
message: 'One document found|{count} documents found',
|
||||||
},
|
},
|
||||||
{ count },
|
{ count }
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useDocsSearchVersionsHelpers() {
|
|
||||||
const allDocsData = useAllDocsData();
|
|
||||||
|
|
||||||
// State of the version select menus / algolia facet filters
|
|
||||||
// docsPluginId -> versionName map
|
|
||||||
const [searchVersions, setSearchVersions] = useState<{
|
|
||||||
[pluginId: string]: string;
|
|
||||||
}>(() =>
|
|
||||||
Object.entries(allDocsData).reduce(
|
|
||||||
(acc, [pluginId, pluginData]) => ({
|
|
||||||
...acc,
|
|
||||||
[pluginId]: pluginData.versions[0]!.name,
|
|
||||||
}),
|
|
||||||
{},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set the value of a single select menu
|
|
||||||
const setSearchVersion = (pluginId: string, searchVersion: string) =>
|
|
||||||
setSearchVersions((s) => ({ ...s, [pluginId]: searchVersion }));
|
|
||||||
|
|
||||||
const versioningEnabled = Object.values(allDocsData).some((docsData) => docsData.versions.length > 1);
|
|
||||||
|
|
||||||
return {
|
|
||||||
allDocsData,
|
|
||||||
versioningEnabled,
|
|
||||||
searchVersions,
|
|
||||||
setSearchVersion,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to display one select per versioned docs plugin instance
|
|
||||||
function SearchVersionSelectList({
|
|
||||||
docsSearchVersionsHelpers,
|
|
||||||
}: {
|
|
||||||
docsSearchVersionsHelpers: ReturnType<typeof useDocsSearchVersionsHelpers>;
|
|
||||||
}) {
|
|
||||||
const versionedPluginEntries = Object.entries(docsSearchVersionsHelpers.allDocsData)
|
|
||||||
// Do not show a version select for unversioned docs plugin instances
|
|
||||||
.filter(([, docsData]) => docsData.versions.length > 1);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={clsx('col', 'col--3', 'padding-left--none', styles.searchVersionColumn)}>
|
|
||||||
{versionedPluginEntries.map(([pluginId, docsData]) => {
|
|
||||||
const labelPrefix = versionedPluginEntries.length > 1 ? `${pluginId}: ` : '';
|
|
||||||
return (
|
|
||||||
<select
|
|
||||||
key={pluginId}
|
|
||||||
defaultValue={docsSearchVersionsHelpers.searchVersions[pluginId]}
|
|
||||||
className={styles.searchVersionInput}
|
|
||||||
onBlur={(e) => docsSearchVersionsHelpers.setSearchVersion(pluginId, e.target.value)}
|
|
||||||
>
|
|
||||||
{docsData.versions.map((version, i) => (
|
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
|
||||||
<option key={i} label={`${labelPrefix}${version.label}`} value={version.name} />
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlgoliaLogo(): ReactNode {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
width="80"
|
|
||||||
height="24"
|
|
||||||
aria-label="Algolia"
|
|
||||||
role="img"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 2196.2 500"
|
|
||||||
style={{ maxWidth: '150px' }}
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<style>{`.cls-1,.cls-2{fill:#003dff}.cls-2{fill-rule:evenodd}`}</style>
|
|
||||||
</defs>
|
|
||||||
<path
|
|
||||||
className="cls-2"
|
|
||||||
d="M1070.38,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"
|
|
||||||
/>
|
|
||||||
<rect className="cls-1" x="1845.88" y="104.73" width="62.58" height="277.9" rx="5.9" ry="5.9" />
|
|
||||||
<path
|
|
||||||
className="cls-2"
|
|
||||||
d="M1851.78,71.38h50.77c3.26,0,5.9-2.64,5.9-5.9V5.9c0-3.62-3.24-6.39-6.82-5.83l-50.77,7.95c-2.87,.45-4.99,2.92-4.99,5.83v51.62c0,3.26,2.64,5.9,5.9,5.9Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="cls-2"
|
|
||||||
d="M1764.03,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="cls-2"
|
|
||||||
d="M1631.95,142.72c-11.14-12.25-24.83-21.65-40.78-28.31-15.92-6.53-33.26-9.85-52.07-9.85-18.78,0-36.15,3.17-51.92,9.85-15.59,6.66-29.29,16.05-40.76,28.31-11.47,12.23-20.38,26.87-26.76,44.03-6.38,17.17-9.24,37.37-9.24,58.36,0,20.99,3.19,36.87,9.55,54.21,6.38,17.32,15.14,32.11,26.45,44.36,11.29,12.23,24.83,21.62,40.6,28.46,15.77,6.83,40.12,10.33,52.4,10.48,12.25,0,36.78-3.82,52.7-10.48,15.92-6.68,29.46-16.23,40.78-28.46,11.29-12.25,20.05-27.04,26.25-44.36,6.22-17.34,9.24-33.22,9.24-54.21,0-20.99-3.34-41.19-10.03-58.36-6.38-17.17-15.14-31.8-26.43-44.03Zm-44.43,163.75c-11.47,15.75-27.56,23.7-48.09,23.7-20.55,0-36.63-7.8-48.1-23.7-11.47-15.75-17.21-34.01-17.21-61.2,0-26.89,5.59-49.14,17.06-64.87,11.45-15.75,27.54-23.52,48.07-23.52,20.55,0,36.63,7.78,48.09,23.52,11.47,15.57,17.36,37.98,17.36,64.87,0,27.19-5.72,45.3-17.19,61.2Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="cls-2"
|
|
||||||
d="M894.42,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="cls-2"
|
|
||||||
d="M2133.97,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="cls-2"
|
|
||||||
d="M1314.05,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-11.79,18.34-19.6,39.64-22.11,62.59-.58,5.3-.88,10.68-.88,16.14s.31,11.15,.93,16.59c4.28,38.09,23.14,71.61,50.66,94.52,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47h0c17.99,0,34.61-5.93,48.16-15.97,16.29-11.58,28.88-28.54,34.48-47.75v50.26h-.11v11.08c0,21.84-5.71,38.27-17.34,49.36-11.61,11.08-31.04,16.63-58.25,16.63-11.12,0-28.79-.59-46.6-2.41-2.83-.29-5.46,1.5-6.27,4.22l-12.78,43.11c-1.02,3.46,1.27,7.02,4.83,7.53,21.52,3.08,42.52,4.68,54.65,4.68,48.91,0,85.16-10.75,108.89-32.21,21.48-19.41,33.15-48.89,35.2-88.52V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,64.1s.65,139.13,0,143.36c-12.08,9.77-27.11,13.59-43.49,14.7-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-1.32,0-2.63-.03-3.94-.1-40.41-2.11-74.52-37.26-74.52-79.38,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="cls-1"
|
|
||||||
d="M249.83,0C113.3,0,2,110.09,.03,246.16c-2,138.19,110.12,252.7,248.33,253.5,42.68,.25,83.79-10.19,120.3-30.03,3.56-1.93,4.11-6.83,1.08-9.51l-23.38-20.72c-4.75-4.21-11.51-5.4-17.36-2.92-25.48,10.84-53.17,16.38-81.71,16.03-111.68-1.37-201.91-94.29-200.13-205.96,1.76-110.26,92-199.41,202.67-199.41h202.69V407.41l-115-102.18c-3.72-3.31-9.42-2.66-12.42,1.31-18.46,24.44-48.53,39.64-81.93,37.34-46.33-3.2-83.87-40.5-87.34-86.81-4.15-55.24,39.63-101.52,94-101.52,49.18,0,89.68,37.85,93.91,85.95,.38,4.28,2.31,8.27,5.52,11.12l29.95,26.55c3.4,3.01,8.79,1.17,9.63-3.3,2.16-11.55,2.92-23.58,2.07-35.92-4.82-70.34-61.8-126.93-132.17-131.26-80.68-4.97-148.13,58.14-150.27,137.25-2.09,77.1,61.08,143.56,138.19,145.26,32.19,.71,62.03-9.41,86.14-26.95l150.26,133.2c6.44,5.71,16.61,1.14,16.61-7.47V9.48C499.66,4.25,495.42,0,490.18,0H249.83Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResultDispatcherState = {
|
|
||||||
items: Array<{
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
summary: string;
|
|
||||||
breadcrumbs: string[];
|
|
||||||
}>;
|
|
||||||
query: string | null;
|
|
||||||
totalResults: number | null;
|
|
||||||
totalPages: number | null;
|
|
||||||
lastPage: number | null;
|
|
||||||
hasMore: boolean | null;
|
|
||||||
loading: boolean | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ResultDispatcher =
|
|
||||||
| { type: 'advance'; value?: undefined }
|
|
||||||
| { type: 'loading'; value?: undefined }
|
|
||||||
| { type: 'reset'; value?: undefined }
|
|
||||||
| { type: 'update'; value: ResultDispatcherState };
|
|
||||||
|
|
||||||
function getSearchPageTitle(searchQuery: string | undefined): string {
|
function getSearchPageTitle(searchQuery: string | undefined): string {
|
||||||
return searchQuery
|
return searchQuery
|
||||||
? translate(
|
? translate(
|
||||||
|
|
@ -188,7 +57,7 @@ function getSearchPageTitle(searchQuery: string | undefined): string {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
query: searchQuery,
|
query: searchQuery,
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
: translate({
|
: translate({
|
||||||
id: 'theme.SearchPage.emptyResultsTitle',
|
id: 'theme.SearchPage.emptyResultsTitle',
|
||||||
|
|
@ -199,184 +68,148 @@ function getSearchPageTitle(searchQuery: string | undefined): string {
|
||||||
|
|
||||||
function SearchPageContent(): ReactNode {
|
function SearchPageContent(): ReactNode {
|
||||||
const {
|
const {
|
||||||
i18n: { currentLocale },
|
searchQuery,
|
||||||
} = useDocusaurusContext();
|
setSearchQuery,
|
||||||
const { appId, apiKey, indexName, contextualSearch } = useAlgoliaThemeConfig();
|
items,
|
||||||
const processSearchResultUrl = useSearchResultUrlProcessor();
|
totalResults,
|
||||||
|
loading,
|
||||||
|
loadingMore,
|
||||||
|
error,
|
||||||
|
hasMore,
|
||||||
|
facets,
|
||||||
|
refinements,
|
||||||
|
toggleRefinement,
|
||||||
|
clearRefinements,
|
||||||
|
hasActiveRefinements,
|
||||||
|
loadMore,
|
||||||
|
sendResultClick,
|
||||||
|
versionHelpers,
|
||||||
|
} = useSearchPage();
|
||||||
|
const { contextualSearch } = useAlgoliaThemeConfig();
|
||||||
const documentsFoundPlural = useDocumentsFoundPlural();
|
const documentsFoundPlural = useDocumentsFoundPlural();
|
||||||
|
const {
|
||||||
|
recentSearches,
|
||||||
|
addRecentSearch,
|
||||||
|
removeRecentSearch,
|
||||||
|
clearRecentSearches,
|
||||||
|
} = useRecentSearches();
|
||||||
|
const topSections = useTopSections();
|
||||||
|
|
||||||
const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers();
|
|
||||||
const [searchQuery, setSearchQuery] = useSearchQueryString();
|
|
||||||
const pageTitle = getSearchPageTitle(searchQuery);
|
const pageTitle = getSearchPageTitle(searchQuery);
|
||||||
|
const showVersionSelects =
|
||||||
|
contextualSearch && versionHelpers.versioningEnabled;
|
||||||
|
const hasSidebar = showVersionSelects || facets.length > 0;
|
||||||
|
const isSearching = Boolean(searchQuery) || hasActiveRefinements;
|
||||||
|
|
||||||
const initialSearchResultState: ResultDispatcherState = {
|
const handleSelectResult = (
|
||||||
items: [],
|
item: SearchResultItem,
|
||||||
query: null,
|
position: number
|
||||||
totalResults: null,
|
): void => {
|
||||||
totalPages: null,
|
sendResultClick(item, position);
|
||||||
lastPage: null,
|
if (searchQuery) {
|
||||||
hasMore: null,
|
addRecentSearch(searchQuery);
|
||||||
loading: null,
|
}
|
||||||
};
|
};
|
||||||
const [searchResultState, searchResultStateDispatcher] = useReducer(
|
|
||||||
(prevState: ResultDispatcherState, data: ResultDispatcher) => {
|
function renderMainContent(): ReactNode {
|
||||||
switch (data.type) {
|
if (!isSearching) {
|
||||||
case 'reset': {
|
return (
|
||||||
return initialSearchResultState;
|
<SearchEmptyState
|
||||||
}
|
recentSearches={recentSearches}
|
||||||
case 'loading': {
|
topSections={topSections}
|
||||||
return { ...prevState, loading: true };
|
onSelectRecent={setSearchQuery}
|
||||||
}
|
onRemoveRecent={removeRecentSearch}
|
||||||
case 'update': {
|
onClearRecent={clearRecentSearches}
|
||||||
if (searchQuery !== data.value.query) {
|
onSelectSection={(section) =>
|
||||||
return prevState;
|
toggleRefinement(TOP_SECTIONS_FACET, section)
|
||||||
}
|
}
|
||||||
|
/>
|
||||||
return {
|
);
|
||||||
...data.value,
|
|
||||||
items: data.value.lastPage === 0 ? data.value.items : prevState.items.concat(data.value.items),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'advance': {
|
|
||||||
const hasMore = prevState.totalPages! > prevState.lastPage! + 1;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prevState,
|
|
||||||
lastPage: hasMore ? prevState.lastPage! + 1 : prevState.lastPage,
|
|
||||||
hasMore,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return prevState;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
initialSearchResultState,
|
|
||||||
);
|
|
||||||
|
|
||||||
// respect settings from the theme config for facets
|
|
||||||
const disjunctiveFacets = contextualSearch ? ['language', 'docusaurus_tag'] : [];
|
|
||||||
|
|
||||||
const algoliaClient = liteClient(appId, apiKey);
|
|
||||||
const algoliaHelper = algoliaSearchHelper(algoliaClient, indexName, {
|
|
||||||
hitsPerPage: 15,
|
|
||||||
advancedSyntax: true,
|
|
||||||
disjunctiveFacets,
|
|
||||||
});
|
|
||||||
|
|
||||||
algoliaHelper.on('result', ({ results: { query, hits, page, nbHits, nbPages } }) => {
|
|
||||||
if (query === '' || !Array.isArray(hits)) {
|
|
||||||
searchResultStateDispatcher({ type: 'reset' });
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitizeValue = (value: string) =>
|
if (error) {
|
||||||
value.replace(/algolia-docsearch-suggestion--highlight/g, 'search-result-match');
|
return (
|
||||||
|
<div className={styles.stateMessage}>
|
||||||
|
<Translate
|
||||||
|
id="theme.SearchPage.errorText"
|
||||||
|
description="The paragraph shown when the search request fails"
|
||||||
|
>
|
||||||
|
Something went wrong while searching. Please try again.
|
||||||
|
</Translate>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const items = hits.map(
|
if (loading && items.length === 0) {
|
||||||
({
|
return <SearchResultsSkeleton />;
|
||||||
url,
|
}
|
||||||
_highlightResult: { hierarchy },
|
|
||||||
_snippetResult: snippet = {},
|
if (items.length > 0) {
|
||||||
}: {
|
return (
|
||||||
url: string;
|
<>
|
||||||
_highlightResult: { hierarchy: { [key: string]: { value: string } } };
|
<SearchResults items={items} onSelect={handleSelectResult} />
|
||||||
_snippetResult: { content?: { value: string } };
|
{hasMore && (
|
||||||
}) => {
|
<div className={styles.loadMoreWrapper}>
|
||||||
const titles = Object.keys(hierarchy).map((key) => sanitizeValue(hierarchy[key]!.value));
|
<button
|
||||||
return {
|
type="button"
|
||||||
title: titles.pop()!,
|
className={clsx(
|
||||||
url: processSearchResultUrl(url),
|
'button',
|
||||||
summary: snippet.content ? `${sanitizeValue(snippet.content.value)}...` : '',
|
'button--secondary',
|
||||||
breadcrumbs: titles,
|
'button--lg',
|
||||||
};
|
styles.loadMoreButton
|
||||||
},
|
)}
|
||||||
|
disabled={loadingMore}
|
||||||
|
onClick={loadMore}
|
||||||
|
>
|
||||||
|
{loadingMore ? (
|
||||||
|
<Translate
|
||||||
|
id="theme.SearchPage.fetchingNewResults"
|
||||||
|
description="The label for the load-more button while fetching new results"
|
||||||
|
>
|
||||||
|
Fetching new results...
|
||||||
|
</Translate>
|
||||||
|
) : (
|
||||||
|
<Translate
|
||||||
|
id="theme.SearchPage.loadMore"
|
||||||
|
description="The label for the button that loads more search results"
|
||||||
|
>
|
||||||
|
Load more results
|
||||||
|
</Translate>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.stateMessage}>
|
||||||
|
<p>
|
||||||
|
<Translate
|
||||||
|
id="theme.SearchPage.noResultsText"
|
||||||
|
description="The paragraph for empty search result"
|
||||||
|
>
|
||||||
|
No results were found
|
||||||
|
</Translate>
|
||||||
|
</p>
|
||||||
|
{hasActiveRefinements && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={clsx('button', 'button--secondary')}
|
||||||
|
onClick={clearRefinements}
|
||||||
|
>
|
||||||
|
<Translate
|
||||||
|
id="theme.SearchPage.clearFiltersFromNoResults"
|
||||||
|
description="The label for the button that clears filters when there are no results"
|
||||||
|
>
|
||||||
|
Clear all filters
|
||||||
|
</Translate>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
searchResultStateDispatcher({
|
|
||||||
type: 'update',
|
|
||||||
value: {
|
|
||||||
items,
|
|
||||||
query,
|
|
||||||
totalResults: nbHits,
|
|
||||||
totalPages: nbPages,
|
|
||||||
lastPage: page,
|
|
||||||
hasMore: nbPages > page + 1,
|
|
||||||
loading: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const [loaderRef, setLoaderRef] = useState<HTMLDivElement | null>(null);
|
|
||||||
const prevY = useRef(0);
|
|
||||||
const observer = useRef(
|
|
||||||
ExecutionEnvironment.canUseIntersectionObserver &&
|
|
||||||
new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
const {
|
|
||||||
isIntersecting,
|
|
||||||
boundingClientRect: { y: currentY },
|
|
||||||
} = entries[0]!;
|
|
||||||
|
|
||||||
if (isIntersecting && prevY.current > currentY) {
|
|
||||||
searchResultStateDispatcher({ type: 'advance' });
|
|
||||||
}
|
|
||||||
|
|
||||||
prevY.current = currentY;
|
|
||||||
},
|
|
||||||
{ threshold: 1 },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const makeSearch = useEvent((page: number = 0) => {
|
|
||||||
if (contextualSearch) {
|
|
||||||
algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default');
|
|
||||||
algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale);
|
|
||||||
|
|
||||||
Object.entries(docsSearchVersionsHelpers.searchVersions).forEach(([pluginId, searchVersion]) => {
|
|
||||||
algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', `docs-${pluginId}-${searchVersion}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
algoliaHelper.setQuery(searchQuery).setPage(page).search();
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!loaderRef) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const currentObserver = observer.current;
|
|
||||||
if (currentObserver) {
|
|
||||||
currentObserver.observe(loaderRef);
|
|
||||||
return () => currentObserver.unobserve(loaderRef);
|
|
||||||
}
|
|
||||||
return () => true;
|
|
||||||
}, [loaderRef]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
searchResultStateDispatcher({ type: 'reset' });
|
|
||||||
|
|
||||||
if (!searchQuery) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchResultStateDispatcher({ type: 'loading' });
|
|
||||||
|
|
||||||
const searchTimeoutId = setTimeout(() => {
|
|
||||||
makeSearch();
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(searchTimeoutId);
|
|
||||||
};
|
|
||||||
}, [searchQuery, docsSearchVersionsHelpers.searchVersions, makeSearch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!searchResultState.lastPage || searchResultState.lastPage === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
makeSearch(searchResultState.lastPage);
|
|
||||||
}, [makeSearch, searchResultState.lastPage]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
|
|
@ -390,127 +223,62 @@ function SearchPageContent(): ReactNode {
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<div className="container margin-vert--lg">
|
<div className="container margin-vert--lg">
|
||||||
<Heading as="h1">{pageTitle}</Heading>
|
<header className={styles.pageHeader}>
|
||||||
|
<Heading as="h1" className={styles.pageTitle}>
|
||||||
|
{pageTitle}
|
||||||
|
</Heading>
|
||||||
|
<PoweredByAlgolia />
|
||||||
|
</header>
|
||||||
|
|
||||||
<form className="row" onSubmit={(e) => e.preventDefault()}>
|
<form
|
||||||
<div
|
className={styles.searchForm}
|
||||||
className={clsx('col', styles.searchQueryColumn, {
|
role="search"
|
||||||
'col--9': docsSearchVersionsHelpers.versioningEnabled,
|
onSubmit={(event) => event.preventDefault()}
|
||||||
'col--12': !docsSearchVersionsHelpers.versioningEnabled,
|
>
|
||||||
})}
|
<SearchInput
|
||||||
>
|
autoFocus={true}
|
||||||
<input
|
value={searchQuery}
|
||||||
autoFocus={true}
|
onChange={setSearchQuery}
|
||||||
type="search"
|
onClear={() => setSearchQuery('')}
|
||||||
name="q"
|
/>
|
||||||
className={styles.searchQueryInput}
|
|
||||||
placeholder={translate({
|
|
||||||
id: 'theme.SearchPage.inputPlaceholder',
|
|
||||||
message: 'Type your search here',
|
|
||||||
description: 'The placeholder for search page input',
|
|
||||||
})}
|
|
||||||
aria-label={translate({
|
|
||||||
id: 'theme.SearchPage.inputLabel',
|
|
||||||
message: 'Search',
|
|
||||||
description: 'The ARIA label for search page input',
|
|
||||||
})}
|
|
||||||
value={searchQuery}
|
|
||||||
autoComplete="off"
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{contextualSearch && docsSearchVersionsHelpers.versioningEnabled && (
|
|
||||||
<SearchVersionSelectList docsSearchVersionsHelpers={docsSearchVersionsHelpers} />
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="row">
|
<div
|
||||||
<div className={clsx('col', 'col--8', styles.searchResultsColumn)}>
|
className={clsx(
|
||||||
{searchResultState.totalResults !== null && documentsFoundPlural(searchResultState.totalResults)}
|
styles.layout,
|
||||||
</div>
|
hasSidebar && styles.layoutWithSidebar
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{hasSidebar && (
|
||||||
|
<SearchFacets
|
||||||
|
facets={facets}
|
||||||
|
refinements={refinements}
|
||||||
|
hasActiveRefinements={hasActiveRefinements}
|
||||||
|
showVersionSelects={showVersionSelects}
|
||||||
|
versionHelpers={versionHelpers}
|
||||||
|
onToggle={toggleRefinement}
|
||||||
|
onClear={clearRefinements}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={clsx('col', 'col--4', styles.searchLogoColumn)}>
|
<div className={styles.mainColumn}>
|
||||||
<span>
|
{isSearching && (totalResults !== null || hasActiveRefinements) && (
|
||||||
{translate({
|
<div className={styles.resultsMeta}>
|
||||||
id: 'theme.SearchPage.algoliaLabel',
|
{totalResults !== null && (
|
||||||
message: 'Powered by',
|
<span className={styles.resultsCount}>
|
||||||
description: 'The text explain that the search powered by Algolia',
|
{documentsFoundPlural(totalResults)}
|
||||||
})}
|
</span>
|
||||||
</span>
|
)}
|
||||||
<Link
|
<ActiveRefinements
|
||||||
to="https://www.algolia.com/"
|
refinements={refinements}
|
||||||
aria-label={translate({
|
onRemove={toggleRefinement}
|
||||||
id: 'theme.SearchPage.algoliaLabel',
|
/>
|
||||||
message: 'Powered by Algolia',
|
</div>
|
||||||
description: 'The description label for Algolia mention',
|
)}
|
||||||
})}
|
|
||||||
>
|
{renderMainContent()}
|
||||||
<AlgoliaLogo />
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{searchResultState.items.length > 0 ? (
|
|
||||||
<main>
|
|
||||||
{searchResultState.items.map(({ title, url, summary, breadcrumbs }, i) => (
|
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
|
||||||
<article key={i} className={styles.searchResultItem}>
|
|
||||||
<Heading as="h2" className={styles.searchResultItemHeading}>
|
|
||||||
<Link to={url} dangerouslySetInnerHTML={{ __html: title }} />
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
{breadcrumbs.length > 0 && (
|
|
||||||
<nav aria-label="breadcrumbs">
|
|
||||||
<ul className={clsx('breadcrumbs', styles.searchResultItemPath)}>
|
|
||||||
{breadcrumbs.map((html, index) => (
|
|
||||||
<li
|
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
|
||||||
key={index}
|
|
||||||
className="breadcrumbs__item"
|
|
||||||
// Developer provided the HTML, so assume it's safe.
|
|
||||||
// eslint-disable-next-line react/no-danger
|
|
||||||
dangerouslySetInnerHTML={{ __html: html }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{summary && (
|
|
||||||
<p
|
|
||||||
className={styles.searchResultItemSummary}
|
|
||||||
// Developer provided the HTML, so assume it's safe.
|
|
||||||
// eslint-disable-next-line react/no-danger
|
|
||||||
dangerouslySetInnerHTML={{ __html: summary }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</main>
|
|
||||||
) : (
|
|
||||||
[
|
|
||||||
searchQuery && !searchResultState.loading && (
|
|
||||||
<p key="no-results">
|
|
||||||
<Translate id="theme.SearchPage.noResultsText" description="The paragraph for empty search result">
|
|
||||||
No results were found
|
|
||||||
</Translate>
|
|
||||||
</p>
|
|
||||||
),
|
|
||||||
Boolean(searchResultState.loading) && <div key="spinner" className={styles.loadingSpinner} />,
|
|
||||||
]
|
|
||||||
)}
|
|
||||||
|
|
||||||
{searchResultState.hasMore && (
|
|
||||||
<div className={styles.loader} ref={setLoaderRef}>
|
|
||||||
<Translate
|
|
||||||
id="theme.SearchPage.fetchingNewResults"
|
|
||||||
description="The paragraph for fetching new search results"
|
|
||||||
>
|
|
||||||
Fetching new results...
|
|
||||||
</Translate>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* stylelint-disable selector-class-pattern -- CSS module class names are camelCased locals */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
*
|
*
|
||||||
|
|
@ -5,123 +7,680 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.searchQueryInput,
|
/* Header */
|
||||||
.searchVersionInput {
|
|
||||||
border-radius: var(--ifm-global-radius);
|
.pageHeader {
|
||||||
border: 2px solid var(--ifm-toc-border-color);
|
display: flex;
|
||||||
font: var(--ifm-font-size-base) var(--ifm-font-family-base);
|
flex-wrap: wrap;
|
||||||
padding: 0.8rem;
|
align-items: baseline;
|
||||||
width: 100%;
|
justify-content: space-between;
|
||||||
background: var(--docsearch-searchbox-focus-background);
|
gap: 1rem;
|
||||||
color: var(--docsearch-text-color);
|
margin-bottom: 1.5rem;
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
transition: border var(--ifm-transition-fast) ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchQueryInput:focus,
|
.pageTitle {
|
||||||
.searchVersionInput:focus {
|
margin: 0;
|
||||||
border-color: var(--docsearch-primary-color);
|
|
||||||
outline: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchQueryInput::placeholder {
|
.poweredBy {
|
||||||
color: var(--docsearch-muted-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchResultsColumn {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchLogoColumn {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchLogoColumn a {
|
.poweredByLabel {
|
||||||
|
color: var(--ifm-color-content-secondary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poweredByLink {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchLogoColumn span {
|
.algoliaLogo {
|
||||||
color: var(--docsearch-muted-color);
|
max-width: 120px;
|
||||||
font-weight: normal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchResultItem {
|
/* Search box */
|
||||||
padding: 1rem 0;
|
|
||||||
border-bottom: 1px solid var(--ifm-toc-border-color);
|
.searchForm {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchResultItemHeading {
|
.searchBox {
|
||||||
font-weight: 400;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0 0.85rem;
|
||||||
|
border: 1px solid var(--ifm-color-emphasis-300);
|
||||||
|
border-radius: var(--ifm-global-radius);
|
||||||
|
background: var(--ifm-background-surface-color);
|
||||||
|
transition: border-color var(--ifm-transition-fast) ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchBox:focus-within {
|
||||||
|
border-color: var(--ifm-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchBoxIcon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--ifm-color-content-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchInput {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0.85rem 0.25rem;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ifm-font-color-base);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchInput:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchInput::placeholder {
|
||||||
|
color: var(--ifm-color-content-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchBoxClear {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0.25rem;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ifm-color-content-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchBoxClear:hover {
|
||||||
|
color: var(--ifm-font-color-base);
|
||||||
|
background: var(--ifm-color-emphasis-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainColumn {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Results meta / active refinements */
|
||||||
|
|
||||||
|
.resultsMeta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultsCount {
|
||||||
|
color: var(--ifm-color-content-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activeRefinements {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activeRefinement {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.25rem 0.6rem;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 2rem;
|
||||||
|
background: var(--ifm-color-emphasis-200);
|
||||||
|
color: var(--ifm-font-color-base);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activeRefinement:hover {
|
||||||
|
background: var(--ifm-color-emphasis-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Result cards */
|
||||||
|
|
||||||
|
.resultList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultLink {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--ifm-global-radius);
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
transition:
|
||||||
|
background-color var(--ifm-transition-fast) ease,
|
||||||
|
border-color var(--ifm-transition-fast) ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultLink:hover {
|
||||||
|
border-color: var(--ifm-color-emphasis-200);
|
||||||
|
background: var(--ifm-color-emphasis-100);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultIconWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-top: 0.15rem;
|
||||||
|
color: var(--ifm-color-content-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultIcon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultContent {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultBreadcrumbs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
color: var(--ifm-color-content-secondary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultBreadcrumbSeparator {
|
||||||
|
color: var(--ifm-color-emphasis-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultTitle {
|
||||||
|
color: var(--ifm-heading-color);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultSummary {
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--ifm-color-content-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultArrow {
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-self: center;
|
||||||
|
color: var(--ifm-color-content-secondary);
|
||||||
|
opacity: 0;
|
||||||
|
transition:
|
||||||
|
opacity var(--ifm-transition-fast) ease,
|
||||||
|
transform var(--ifm-transition-fast) ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultLink:hover .resultArrow {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0.15rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar / facets */
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersToggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 0.65rem 0.85rem;
|
||||||
|
border: 1px solid var(--ifm-color-emphasis-300);
|
||||||
|
border-radius: var(--ifm-global-radius);
|
||||||
|
background: var(--ifm-background-surface-color);
|
||||||
|
color: var(--ifm-font-color-base);
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color var(--ifm-transition-fast) ease,
|
||||||
|
border-radius var(--ifm-transition-fast) ease,
|
||||||
|
margin-bottom var(--ifm-transition-fast) ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersToggleLabel {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersToggleChevron {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--ifm-color-content-secondary);
|
||||||
|
transition: transform var(--ifm-transition-fast) ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersToggleOpen {
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-bottom: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersToggleOpen .filtersToggleChevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersToggleBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 1.4rem;
|
||||||
|
padding: 0 0.4rem;
|
||||||
|
border-radius: 2rem;
|
||||||
|
background: var(--ifm-color-primary);
|
||||||
|
color: var(--ifm-color-white);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarPanel {
|
||||||
|
display: none;
|
||||||
|
padding: 1.25rem;
|
||||||
|
border: 1px solid var(--ifm-color-emphasis-200);
|
||||||
|
border-radius: var(--ifm-global-radius);
|
||||||
|
background: var(--ifm-background-surface-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Higher specificity so this wins over `.sidebarPanel { display: none }` after CSS bundling. */
|
||||||
|
.sidebarPanel.sidebarPanelOpen {
|
||||||
|
display: block;
|
||||||
|
border-top: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarHeaderDesktop {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarHeaderMobileClear {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarTitle {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearFacetsButton {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
color: var(--ifm-color-primary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearFacetsButton:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facetGroup {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facetGroup:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchResultItemPath {
|
.facetLabel {
|
||||||
font-size: 0.8rem;
|
margin: 0 0 0.6rem;
|
||||||
color: var(--ifm-color-content-secondary);
|
color: var(--ifm-color-content-secondary);
|
||||||
--ifm-breadcrumb-separator-size-multiplier: 1;
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchResultItemSummary {
|
.facetList {
|
||||||
margin: 0.5rem 0 0;
|
display: flex;
|
||||||
font-style: italic;
|
flex-direction: column;
|
||||||
|
gap: 0.15rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 996px) {
|
.facetItem {
|
||||||
.searchQueryColumn {
|
display: flex;
|
||||||
max-width: 60% !important;
|
align-items: center;
|
||||||
}
|
gap: 0.6rem;
|
||||||
|
padding: 0.35rem 0.4rem;
|
||||||
.searchVersionColumn {
|
border-radius: var(--ifm-global-radius);
|
||||||
max-width: 40% !important;
|
font-size: 0.9rem;
|
||||||
}
|
cursor: pointer;
|
||||||
|
|
||||||
.searchResultsColumn {
|
|
||||||
max-width: 60% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchLogoColumn {
|
|
||||||
max-width: 40% !important;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 576px) {
|
.facetItem:hover {
|
||||||
.searchQueryColumn {
|
background: var(--ifm-color-emphasis-100);
|
||||||
max-width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchVersionColumn {
|
|
||||||
max-width: 100% !important;
|
|
||||||
padding-left: var(--ifm-spacing-horizontal) !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.loadingSpinner {
|
.facetCheckbox {
|
||||||
width: 3rem;
|
flex-shrink: 0;
|
||||||
height: 3rem;
|
margin: 0;
|
||||||
border: 0.4em solid #eee;
|
accent-color: var(--ifm-color-primary);
|
||||||
border-top-color: var(--ifm-color-primary);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: loading-spin 1s linear infinite;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes loading-spin {
|
.facetName {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facetCount {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 0.4rem;
|
||||||
|
border-radius: var(--ifm-global-radius);
|
||||||
|
background: var(--ifm-color-emphasis-100);
|
||||||
|
color: var(--ifm-color-content-secondary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versionSelect {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--ifm-color-emphasis-300);
|
||||||
|
border-radius: var(--ifm-global-radius);
|
||||||
|
background: var(--ifm-background-color);
|
||||||
|
color: var(--ifm-font-color-base);
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versionSelect:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skeleton */
|
||||||
|
|
||||||
|
.skeletonList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonItem {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonIcon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: var(--ifm-global-radius);
|
||||||
|
background: var(--ifm-color-emphasis-200);
|
||||||
|
animation: search-page-pulse 1.4s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonBody {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonLine {
|
||||||
|
height: 0.75rem;
|
||||||
|
border-radius: var(--ifm-global-radius);
|
||||||
|
background: var(--ifm-color-emphasis-200);
|
||||||
|
animation: search-page-pulse 1.4s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonLineShort {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonLineTitle {
|
||||||
|
width: 55%;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonLineWide {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes search-page-pulse {
|
||||||
|
0%,
|
||||||
100% {
|
100% {
|
||||||
transform: rotate(360deg);
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader {
|
/* Empty state */
|
||||||
margin-top: 2rem;
|
|
||||||
|
.emptyState {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
padding-block: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.search-result-match) {
|
.emptySectionHeader {
|
||||||
color: var(--docsearch-hit-color);
|
display: flex;
|
||||||
background: rgb(255 215 142 / 25%);
|
align-items: center;
|
||||||
padding: 0.09em 0;
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptySectionTitle {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptySectionAction {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
color: var(--ifm-color-primary);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptySectionAction:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recentList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.15rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recentItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recentButton {
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--ifm-global-radius);
|
||||||
|
background: none;
|
||||||
|
color: var(--ifm-font-color-base);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recentButton:hover {
|
||||||
|
background: var(--ifm-color-emphasis-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recentButton svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--ifm-color-content-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recentRemove {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0.35rem;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: none;
|
||||||
|
color: var(--ifm-color-content-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recentRemove:hover {
|
||||||
|
background: var(--ifm-color-emphasis-200);
|
||||||
|
color: var(--ifm-font-color-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionChips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionChip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.4rem 0.85rem;
|
||||||
|
border: 1px solid var(--ifm-color-emphasis-300);
|
||||||
|
border-radius: 2rem;
|
||||||
|
background: var(--ifm-background-surface-color);
|
||||||
|
color: var(--ifm-font-color-base);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color var(--ifm-transition-fast) ease,
|
||||||
|
color var(--ifm-transition-fast) ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionChip:hover {
|
||||||
|
border-color: var(--ifm-color-primary);
|
||||||
|
color: var(--ifm-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionChipCount {
|
||||||
|
color: var(--ifm-color-content-secondary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyHint {
|
||||||
|
color: var(--ifm-color-content-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* States */
|
||||||
|
|
||||||
|
.stateMessage {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 2rem 0;
|
||||||
|
color: var(--ifm-color-content-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadMoreWrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadMoreButton {
|
||||||
|
min-width: 12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlighting */
|
||||||
|
|
||||||
|
/* stylelint-disable-next-line selector-pseudo-class-no-unknown */
|
||||||
|
:global(.search-result-match) {
|
||||||
|
padding: 0.09em 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: rgb(255 215 142 / 25%);
|
||||||
|
color: var(--docsearch-hit-color, inherit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive: two columns and always-visible sidebar on desktop */
|
||||||
|
|
||||||
|
@media (width >= 997px) {
|
||||||
|
.filtersToggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarPanel {
|
||||||
|
display: block;
|
||||||
|
border-top: 1px solid var(--ifm-color-emphasis-200);
|
||||||
|
border-top-left-radius: var(--ifm-global-radius);
|
||||||
|
border-top-right-radius: var(--ifm-global-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarHeaderDesktop {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarHeaderMobileClear {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layoutWithSidebar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 16rem minmax(0, 1fr);
|
||||||
|
gap: 2rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: calc(var(--ifm-navbar-height) + 1rem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
|
* in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type SearchPageFacetConfig = {
|
||||||
|
attribute: string;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SearchResultItem = {
|
||||||
|
objectID: string;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
summary: string;
|
||||||
|
breadcrumbs: string[];
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FacetValueItem = {
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
isRefined: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FacetGroup = {
|
||||||
|
attribute: string;
|
||||||
|
label: string;
|
||||||
|
items: FacetValueItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// attribute -> selected facet values
|
||||||
|
export type Refinements = Record<string, string[]>;
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
|
* in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ThemeConfigDocSearch } from '@docsearch/docusaurus-adapter';
|
||||||
|
import type algoliaSearchHelper from 'algoliasearch-helper';
|
||||||
|
|
||||||
|
type SearchIndex = ThemeConfigDocSearch['indices'][number];
|
||||||
|
type PlainSearchParameters = NonNullable<
|
||||||
|
Parameters<typeof algoliaSearchHelper>[2]
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function getIndexName(index: SearchIndex): string {
|
||||||
|
return typeof index === 'string' ? index : index.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIndexSearchParameters(
|
||||||
|
index: SearchIndex
|
||||||
|
): PlainSearchParameters {
|
||||||
|
return (
|
||||||
|
typeof index === 'string' ? {} : (index.searchParameters ?? {})
|
||||||
|
) as PlainSearchParameters;
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { DocSearchTranslations } from '@docsearch/react';
|
import type { DocSearchTranslations } from '@docsearch/react';
|
||||||
|
|
@ -39,6 +39,7 @@ const translations: DocSearchTranslations & {
|
||||||
askAiScreen: {
|
askAiScreen: {
|
||||||
disclaimerText: string;
|
disclaimerText: string;
|
||||||
relatedSourcesText: string;
|
relatedSourcesText: string;
|
||||||
|
relatedSourcesTextPlural: string;
|
||||||
thinkingText: string;
|
thinkingText: string;
|
||||||
copyButtonText: string;
|
copyButtonText: string;
|
||||||
copyButtonCopiedText: string;
|
copyButtonCopiedText: string;
|
||||||
|
|
@ -46,6 +47,17 @@ const translations: DocSearchTranslations & {
|
||||||
likeButtonTitle: string;
|
likeButtonTitle: string;
|
||||||
dislikeButtonTitle: string;
|
dislikeButtonTitle: string;
|
||||||
thanksForFeedbackText: string;
|
thanksForFeedbackText: string;
|
||||||
|
feedbackPanelTitle: string;
|
||||||
|
feedbackDetailsPlaceholder: string;
|
||||||
|
feedbackDisclaimerText: string;
|
||||||
|
feedbackSubmitButtonText: string;
|
||||||
|
feedbackCloseButtonTitle: string;
|
||||||
|
feedbackTagIncorrect: string;
|
||||||
|
feedbackTagNotWhatIAsked: string;
|
||||||
|
feedbackTagSlowOrBuggy: string;
|
||||||
|
feedbackTagStyleOrTone: string;
|
||||||
|
feedbackTagSafetyOrLegal: string;
|
||||||
|
feedbackTagOther: string;
|
||||||
preToolCallText: string;
|
preToolCallText: string;
|
||||||
duringToolCallText: string;
|
duringToolCallText: string;
|
||||||
afterToolCallText: string;
|
afterToolCallText: string;
|
||||||
|
|
@ -127,7 +139,8 @@ const translations: DocSearchTranslations & {
|
||||||
placeholderTextAskAiStreaming: translate({
|
placeholderTextAskAiStreaming: translate({
|
||||||
id: 'theme.SearchModal.searchBox.placeholderTextAskAiStreaming',
|
id: 'theme.SearchModal.searchBox.placeholderTextAskAiStreaming',
|
||||||
message: 'Answering...',
|
message: 'Answering...',
|
||||||
description: 'The placeholder text for search box when AI is streaming an answer',
|
description:
|
||||||
|
'The placeholder text for search box when AI is streaming an answer',
|
||||||
}),
|
}),
|
||||||
enterKeyHint: translate({
|
enterKeyHint: translate({
|
||||||
id: 'theme.SearchModal.searchBox.enterKeyHint',
|
id: 'theme.SearchModal.searchBox.enterKeyHint',
|
||||||
|
|
@ -219,13 +232,19 @@ const translations: DocSearchTranslations & {
|
||||||
askAiScreen: {
|
askAiScreen: {
|
||||||
disclaimerText: translate({
|
disclaimerText: translate({
|
||||||
id: 'theme.SearchModal.askAiScreen.disclaimerText',
|
id: 'theme.SearchModal.askAiScreen.disclaimerText',
|
||||||
message: 'Answers are generated with AI which can make mistakes. Verify responses.',
|
message:
|
||||||
|
'Answers are generated with AI which can make mistakes. Verify responses.',
|
||||||
description: 'The disclaimer text for AI answers',
|
description: 'The disclaimer text for AI answers',
|
||||||
}),
|
}),
|
||||||
relatedSourcesText: translate({
|
relatedSourcesText: translate({
|
||||||
id: 'theme.SearchModal.askAiScreen.relatedSourcesText',
|
id: 'theme.SearchModal.askAiScreen.relatedSourcesText',
|
||||||
message: 'Related sources',
|
message: 'Source',
|
||||||
description: 'The text for related sources',
|
description: 'Text shown describing a singular related source',
|
||||||
|
}),
|
||||||
|
relatedSourcesTextPlural: translate({
|
||||||
|
id: 'theme.SearchModal.askAiScreen.relatedSourcesTextPlural',
|
||||||
|
message: 'Sources',
|
||||||
|
description: 'Text shown describing multiple related sources',
|
||||||
}),
|
}),
|
||||||
thinkingText: translate({
|
thinkingText: translate({
|
||||||
id: 'theme.SearchModal.askAiScreen.thinkingText',
|
id: 'theme.SearchModal.askAiScreen.thinkingText',
|
||||||
|
|
@ -262,6 +281,67 @@ const translations: DocSearchTranslations & {
|
||||||
message: 'Thanks for your feedback!',
|
message: 'Thanks for your feedback!',
|
||||||
description: 'The text for thanks for feedback',
|
description: 'The text for thanks for feedback',
|
||||||
}),
|
}),
|
||||||
|
feedbackPanelTitle: translate({
|
||||||
|
id: 'theme.SearchModal.askAiScreen.feedbackPanelTitle',
|
||||||
|
message: 'What went wrong? (optional)',
|
||||||
|
description: 'The title of the negative feedback note panel',
|
||||||
|
}),
|
||||||
|
feedbackDetailsPlaceholder: translate({
|
||||||
|
id: 'theme.SearchModal.askAiScreen.feedbackDetailsPlaceholder',
|
||||||
|
message: 'Share some details...',
|
||||||
|
description:
|
||||||
|
'The placeholder for the negative feedback details textarea',
|
||||||
|
}),
|
||||||
|
feedbackDisclaimerText: translate({
|
||||||
|
id: 'theme.SearchModal.askAiScreen.feedbackDisclaimerText',
|
||||||
|
message: 'A copy of this chat will be included with your feedback.',
|
||||||
|
description:
|
||||||
|
'The disclaimer shown inside the negative feedback note panel',
|
||||||
|
}),
|
||||||
|
feedbackSubmitButtonText: translate({
|
||||||
|
id: 'theme.SearchModal.askAiScreen.feedbackSubmitButtonText',
|
||||||
|
message: 'Submit',
|
||||||
|
description:
|
||||||
|
'The submit button text for the negative feedback note panel',
|
||||||
|
}),
|
||||||
|
feedbackCloseButtonTitle: translate({
|
||||||
|
id: 'theme.SearchModal.askAiScreen.feedbackCloseButtonTitle',
|
||||||
|
message: 'Close',
|
||||||
|
description:
|
||||||
|
'The accessible title for the negative feedback note panel close button',
|
||||||
|
}),
|
||||||
|
feedbackTagIncorrect: translate({
|
||||||
|
id: 'theme.SearchModal.askAiScreen.feedbackTagIncorrect',
|
||||||
|
message: 'Incorrect or incomplete',
|
||||||
|
description: 'The "incorrect" reason chip for negative feedback',
|
||||||
|
}),
|
||||||
|
feedbackTagNotWhatIAsked: translate({
|
||||||
|
id: 'theme.SearchModal.askAiScreen.feedbackTagNotWhatIAsked',
|
||||||
|
message: 'Not what I asked for',
|
||||||
|
description:
|
||||||
|
'The "not what I asked for" reason chip for negative feedback',
|
||||||
|
}),
|
||||||
|
feedbackTagSlowOrBuggy: translate({
|
||||||
|
id: 'theme.SearchModal.askAiScreen.feedbackTagSlowOrBuggy',
|
||||||
|
message: 'Slow or buggy',
|
||||||
|
description: 'The "slow or buggy" reason chip for negative feedback',
|
||||||
|
}),
|
||||||
|
feedbackTagStyleOrTone: translate({
|
||||||
|
id: 'theme.SearchModal.askAiScreen.feedbackTagStyleOrTone',
|
||||||
|
message: 'Style or tone',
|
||||||
|
description: 'The "style or tone" reason chip for negative feedback',
|
||||||
|
}),
|
||||||
|
feedbackTagSafetyOrLegal: translate({
|
||||||
|
id: 'theme.SearchModal.askAiScreen.feedbackTagSafetyOrLegal',
|
||||||
|
message: 'Safety or legal concern',
|
||||||
|
description:
|
||||||
|
'The "safety or legal concern" reason chip for negative feedback',
|
||||||
|
}),
|
||||||
|
feedbackTagOther: translate({
|
||||||
|
id: 'theme.SearchModal.askAiScreen.feedbackTagOther',
|
||||||
|
message: 'Other',
|
||||||
|
description: 'The "other" reason chip for negative feedback',
|
||||||
|
}),
|
||||||
preToolCallText: translate({
|
preToolCallText: translate({
|
||||||
id: 'theme.SearchModal.askAiScreen.preToolCallText',
|
id: 'theme.SearchModal.askAiScreen.preToolCallText',
|
||||||
message: 'Searching...',
|
message: 'Searching...',
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/// <reference types="@docusaurus/module-type-aliases" />
|
/// <reference types="@docusaurus/module-type-aliases" />
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escapes special characters in a string for use in a regular expression.
|
* Escapes special characters in a string for use in a regular expression. Based
|
||||||
* Based on escape-string-regexp package.
|
* on escape-string-regexp package.
|
||||||
*/
|
*/
|
||||||
export function escapeRegexp(string: string): string {
|
export function escapeRegexp(string: string): string {
|
||||||
// Escape characters with special meaning either inside or outside character
|
// Escape characters with special meaning either inside or outside character
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { normalizeUrl } from './normalizeUrl';
|
export { normalizeUrl } from './normalizeUrl';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -10,7 +10,8 @@
|
||||||
* joins them into a reasonable URL.
|
* joins them into a reasonable URL.
|
||||||
*
|
*
|
||||||
* - `["file:", "/home", "/user/", "website"]` => `file:///home/user/website`
|
* - `["file:", "/home", "/user/", "website"]` => `file:///home/user/website`
|
||||||
* - `["file://", "home", "/user/", "website"]` => `file://home/user/website` (relative!)
|
* - `["file://", "home", "/user/", "website"]` => `file://home/user/website`
|
||||||
|
* (relative!)
|
||||||
* - Remove trailing slash before parameters or hash.
|
* - Remove trailing slash before parameters or hash.
|
||||||
* - Replace `?` in query parameters with `&`.
|
* - Replace `?` in query parameters with `&`.
|
||||||
* - Dedupe forward slashes in the entire path, avoiding protocol slashes.
|
* - Dedupe forward slashes in the entire path, avoiding protocol slashes.
|
||||||
|
|
@ -24,7 +25,8 @@ export function normalizeUrl(rawUrls: string[]): string {
|
||||||
let hasStartingSlash = false;
|
let hasStartingSlash = false;
|
||||||
let hasEndingSlash = false;
|
let hasEndingSlash = false;
|
||||||
|
|
||||||
const isNonEmptyArray = (arr: string[]): arr is [string, ...string[]] => arr.length > 0;
|
const isNonEmptyArray = (arr: string[]): arr is [string, ...string[]] =>
|
||||||
|
arr.length > 0;
|
||||||
|
|
||||||
if (!isNonEmptyArray(urls)) {
|
if (!isNonEmptyArray(urls)) {
|
||||||
return '';
|
return '';
|
||||||
|
|
@ -69,7 +71,7 @@ export function normalizeUrl(rawUrls: string[]): string {
|
||||||
/^\/+/,
|
/^\/+/,
|
||||||
// Special case where the first element of rawUrls is empty
|
// Special case where the first element of rawUrls is empty
|
||||||
// ["", "/hello"] => /hello
|
// ["", "/hello"] => /hello
|
||||||
component.startsWith('/') && !hasStartingSlash ? '/' : '',
|
component.startsWith('/') && !hasStartingSlash ? '/' : ''
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,50 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. And its affiliates.
|
* Copyright (c) Facebook, Inc. And its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the LICENSE file
|
||||||
* LICENSE file in the root directory of this source tree.
|
* in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ThemeConfig, ThemeConfigAlgolia } from '@docsearch/docusaurus-adapter';
|
import type {
|
||||||
|
ThemeConfig,
|
||||||
|
ThemeConfigDocSearch,
|
||||||
|
} from '@docsearch/docusaurus-adapter';
|
||||||
import type { ThemeConfigValidationContext } from '@docusaurus/types';
|
import type { ThemeConfigValidationContext } from '@docusaurus/types';
|
||||||
import Joi from 'joi';
|
import Joi from 'joi';
|
||||||
|
|
||||||
import { docSearchVersionString } from './docSearchVersion';
|
|
||||||
import { getDocSearchConfig } from './getDocSearchConfig';
|
|
||||||
import { escapeRegexp } from './utils';
|
import { escapeRegexp } from './utils';
|
||||||
|
|
||||||
export const DEFAULT_CONFIG = {
|
export const DEFAULT_CONFIG = {
|
||||||
// Enabled by default, as it makes sense in most cases
|
// Enabled by default, as it makes sense in most cases
|
||||||
// see also https://github.com/facebook/docusaurus/issues/5880
|
// see also https://github.com/facebook/docusaurus/issues/5880
|
||||||
contextualSearch: true,
|
contextualSearch: true,
|
||||||
searchParameters: {},
|
searchPage: {
|
||||||
searchPagePath: 'search',
|
path: 'search',
|
||||||
} satisfies Partial<ThemeConfigAlgolia>;
|
},
|
||||||
|
} satisfies Partial<ThemeConfigDocSearch>;
|
||||||
|
|
||||||
const FacetFiltersSchema = Joi.array().items(Joi.alternatives().try(Joi.string(), Joi.array().items(Joi.string())));
|
const FacetFiltersSchema = Joi.array().items(
|
||||||
|
Joi.alternatives().try(Joi.string(), Joi.array().items(Joi.string()))
|
||||||
|
);
|
||||||
|
|
||||||
const AskAiSearchParametersSchema = Joi.object({
|
const SearchParametersSchema = Joi.object({
|
||||||
facetFilters: FacetFiltersSchema.optional(),
|
facetFilters: FacetFiltersSchema.optional(),
|
||||||
filters: Joi.string().optional(),
|
filters: Joi.string().optional(),
|
||||||
attributesToRetrieve: Joi.array().items(Joi.string()).optional(),
|
attributesToRetrieve: Joi.array().items(Joi.string()).optional(),
|
||||||
restrictSearchableAttributes: Joi.array().items(Joi.string()).optional(),
|
restrictSearchableAttributes: Joi.array().items(Joi.string()).optional(),
|
||||||
distinct: Joi.alternatives().try(Joi.boolean(), Joi.number(), Joi.string()).optional(),
|
distinct: Joi.alternatives()
|
||||||
|
.try(Joi.boolean(), Joi.number(), Joi.string())
|
||||||
|
.optional(),
|
||||||
}).unknown();
|
}).unknown();
|
||||||
|
|
||||||
|
const AskAiMemorySchema = Joi.object({
|
||||||
|
enabled: Joi.bool().optional().default(false),
|
||||||
|
userToken: Joi.string().optional(),
|
||||||
|
}).unknown(false);
|
||||||
|
|
||||||
const SidePanelKeyboardShortcutsSchema = Joi.object({
|
const SidePanelKeyboardShortcutsSchema = Joi.object({
|
||||||
'Ctrl/Cmd+I': Joi.boolean().optional(),
|
'Ctrl/Cmd+I': Joi.boolean().optional(),
|
||||||
});
|
}).unknown(false);
|
||||||
|
|
||||||
const SidePanelSchema = Joi.object({
|
const SidePanelSchema = Joi.object({
|
||||||
keyboardShortcuts: SidePanelKeyboardShortcutsSchema.optional(),
|
keyboardShortcuts: SidePanelKeyboardShortcutsSchema.optional(),
|
||||||
|
|
@ -45,179 +56,220 @@ const SidePanelSchema = Joi.object({
|
||||||
suggestedQuestions: Joi.boolean().optional(),
|
suggestedQuestions: Joi.boolean().optional(),
|
||||||
translations: Joi.object().optional().unknown(),
|
translations: Joi.object().optional().unknown(),
|
||||||
hideButton: Joi.boolean().optional(),
|
hideButton: Joi.boolean().optional(),
|
||||||
});
|
portalContainer: Joi.object().optional().unknown(),
|
||||||
|
indices: Joi.array().items(Joi.string().min(1)).min(1).optional(),
|
||||||
|
memory: AskAiMemorySchema.optional(),
|
||||||
|
}).unknown(false);
|
||||||
|
|
||||||
export const Schema = Joi.object<ThemeConfig>({
|
const KeyboardShortcutsSchema = Joi.object({
|
||||||
algolia: Joi.object<ThemeConfigAlgolia>({
|
'Ctrl/Cmd+K': Joi.boolean().optional(),
|
||||||
// Docusaurus attributes
|
'/': Joi.boolean().optional(),
|
||||||
contextualSearch: Joi.boolean().default(DEFAULT_CONFIG.contextualSearch),
|
'Ctrl/Cmd+I': Joi.boolean().optional(),
|
||||||
externalUrlRegex: Joi.string().optional(),
|
}).unknown(false);
|
||||||
// Algolia attributes
|
|
||||||
appId: Joi.string().required().messages({
|
|
||||||
'any.required':
|
|
||||||
'"algolia.appId" is required. If you haven\'t migrated to the new DocSearch infra, please refer to the blog post for instructions: https://docusaurus.io/blog/2021/11/21/algolia-docsearch-migration',
|
|
||||||
}),
|
|
||||||
apiKey: Joi.string().required(),
|
|
||||||
indexName: Joi.string().required(),
|
|
||||||
searchParameters: Joi.object({
|
|
||||||
facetFilters: FacetFiltersSchema.optional(),
|
|
||||||
})
|
|
||||||
.default(DEFAULT_CONFIG.searchParameters)
|
|
||||||
.unknown(),
|
|
||||||
searchPagePath: Joi.alternatives()
|
|
||||||
.try(Joi.boolean().invalid(true), Joi.string())
|
|
||||||
.allow(null)
|
|
||||||
.default(DEFAULT_CONFIG.searchPagePath),
|
|
||||||
replaceSearchResultPathname: Joi.object({
|
|
||||||
from: Joi.custom((from) => {
|
|
||||||
if (typeof from === 'string') {
|
|
||||||
return escapeRegexp(from);
|
|
||||||
}
|
|
||||||
if (from instanceof RegExp) {
|
|
||||||
return from.source;
|
|
||||||
}
|
|
||||||
throw new Error(`it should be a RegExp or a string, but received ${from}`);
|
|
||||||
}).required(),
|
|
||||||
to: Joi.string().required(),
|
|
||||||
}).optional(),
|
|
||||||
// Ask AI configuration (DocSearch v4 only)
|
|
||||||
askAi: Joi.alternatives()
|
|
||||||
.try(
|
|
||||||
// Simple string format (assistantId only)
|
|
||||||
Joi.string(),
|
|
||||||
// Full configuration object
|
|
||||||
Joi.object({
|
|
||||||
assistantId: Joi.string().required(),
|
|
||||||
// Optional Ask AI configuration
|
|
||||||
indexName: Joi.string().optional(),
|
|
||||||
apiKey: Joi.string().optional(),
|
|
||||||
appId: Joi.string().optional(),
|
|
||||||
agentStudio: Joi.boolean().optional(),
|
|
||||||
searchParameters: AskAiSearchParametersSchema,
|
|
||||||
suggestedQuestions: Joi.boolean().optional(),
|
|
||||||
sidePanel: Joi.alternatives().try(Joi.boolean(), SidePanelSchema).optional(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.custom((askAiInput: ThemeConfigAlgolia['askAi'] | string | undefined, helpers) => {
|
|
||||||
if (!askAiInput) {
|
|
||||||
return askAiInput;
|
|
||||||
}
|
|
||||||
const algolia: ThemeConfigAlgolia = helpers.state.ancestors[0];
|
|
||||||
const algoliaFacetFilters = algolia.searchParameters?.facetFilters;
|
|
||||||
if (typeof askAiInput === 'string') {
|
|
||||||
return {
|
|
||||||
assistantId: askAiInput,
|
|
||||||
indexName: algolia.indexName,
|
|
||||||
apiKey: algolia.apiKey,
|
|
||||||
appId: algolia.appId,
|
|
||||||
...(algoliaFacetFilters
|
|
||||||
? {
|
|
||||||
searchParameters: {
|
|
||||||
facetFilters: algoliaFacetFilters,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
} satisfies ThemeConfigAlgolia['askAi'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill in missing fields with the top-level Algolia config
|
const IndexSchema = Joi.alternatives().try(
|
||||||
const normalizedAskAi = { ...askAiInput };
|
Joi.string(),
|
||||||
normalizedAskAi.indexName = normalizedAskAi.indexName ?? algolia.indexName;
|
Joi.object({
|
||||||
normalizedAskAi.apiKey = normalizedAskAi.apiKey ?? algolia.apiKey;
|
name: Joi.string().required(),
|
||||||
normalizedAskAi.appId = normalizedAskAi.appId ?? algolia.appId;
|
searchParameters: SearchParametersSchema.optional(),
|
||||||
if (
|
}).unknown(false)
|
||||||
normalizedAskAi.agentStudio !== true &&
|
);
|
||||||
normalizedAskAi.searchParameters?.facetFilters === undefined &&
|
|
||||||
algoliaFacetFilters
|
|
||||||
) {
|
|
||||||
normalizedAskAi.searchParameters = {
|
|
||||||
...(normalizedAskAi.searchParameters ?? {}),
|
|
||||||
facetFilters: algoliaFacetFilters,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizedAskAi;
|
const AskAiPromptSuggestionsSchema = Joi.object({
|
||||||
})
|
indexName: Joi.string().min(1).required(),
|
||||||
.optional()
|
hitsPerPage: Joi.number().positive().optional().default(3),
|
||||||
.messages({
|
}).unknown(false);
|
||||||
'alternatives.types':
|
|
||||||
'askAi must be either a string (assistantId) or an object with indexName, apiKey, appId, and assistantId',
|
const AskAiSchema = Joi.object({
|
||||||
}),
|
agentId: Joi.string().required(),
|
||||||
|
suggestedQuestions: Joi.boolean().optional(),
|
||||||
|
searchParameters: Joi.object()
|
||||||
|
.pattern(Joi.string(), SearchParametersSchema)
|
||||||
|
.optional(),
|
||||||
|
indices: Joi.array().items(Joi.string().min(1)).min(1).optional(),
|
||||||
|
memory: AskAiMemorySchema.optional(),
|
||||||
|
promptSuggestions: AskAiPromptSuggestionsSchema.optional(),
|
||||||
|
}).unknown(false);
|
||||||
|
|
||||||
|
const SearchPageFacetSchema = Joi.object({
|
||||||
|
attribute: Joi.string().required(),
|
||||||
|
label: Joi.string().optional(),
|
||||||
|
}).unknown(false);
|
||||||
|
|
||||||
|
const SearchPageSchema = Joi.alternatives()
|
||||||
|
.try(
|
||||||
|
Joi.boolean().valid(false),
|
||||||
|
Joi.object({
|
||||||
|
path: Joi.string().default(DEFAULT_CONFIG.searchPage.path),
|
||||||
|
facets: Joi.array().items(SearchPageFacetSchema).optional(),
|
||||||
|
}).unknown(false)
|
||||||
|
)
|
||||||
|
.default(DEFAULT_CONFIG.searchPage);
|
||||||
|
|
||||||
|
const DocSearchSchema = Joi.object<ThemeConfigDocSearch>({
|
||||||
|
contextualSearch: Joi.boolean().default(DEFAULT_CONFIG.contextualSearch),
|
||||||
|
externalUrlRegex: Joi.string().optional(),
|
||||||
|
appId: Joi.string().required().messages({
|
||||||
|
'any.required':
|
||||||
|
'"docsearch.appId" is required. If you haven\'t migrated to the new DocSearch infra, please refer to the blog post for instructions: https://docusaurus.io/blog/2021/11/21/algolia-docsearch-migration',
|
||||||
|
}),
|
||||||
|
apiKey: Joi.string().required(),
|
||||||
|
indices: Joi.array().items(IndexSchema).min(1).required(),
|
||||||
|
facets: Joi.array()
|
||||||
|
.items(
|
||||||
|
Joi.object({
|
||||||
|
key: Joi.string().required(),
|
||||||
|
label: Joi.string().optional(),
|
||||||
|
}).unknown(false)
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
initialQuery: Joi.string().optional(),
|
||||||
|
insights: Joi.alternatives()
|
||||||
|
.try(Joi.boolean(), Joi.object().unknown())
|
||||||
|
.optional(),
|
||||||
|
placeholder: Joi.string().optional(),
|
||||||
|
translations: Joi.object().optional().unknown(),
|
||||||
|
maxResultsPerGroup: Joi.number().optional(),
|
||||||
|
disableUserPersonalization: Joi.boolean().optional(),
|
||||||
|
getMissingResultsUrl: Joi.function().optional(),
|
||||||
|
keyboardShortcuts: KeyboardShortcutsSchema.optional(),
|
||||||
|
recentSearchesLimit: Joi.number().optional(),
|
||||||
|
recentSearchesWithFavoritesLimit: Joi.number().optional(),
|
||||||
|
resultBadgeKey: Joi.string().optional(),
|
||||||
|
replaceSearchResultPathname: Joi.object({
|
||||||
|
from: Joi.custom((from) => {
|
||||||
|
if (typeof from === 'string') {
|
||||||
|
return escapeRegexp(from);
|
||||||
|
}
|
||||||
|
if (from instanceof RegExp) {
|
||||||
|
return from.source;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`it should be a RegExp or a string, but received ${from}`
|
||||||
|
);
|
||||||
|
}).required(),
|
||||||
|
to: Joi.string().required(),
|
||||||
})
|
})
|
||||||
.label('themeConfig.algolia')
|
.unknown(false)
|
||||||
.required()
|
.optional(),
|
||||||
.unknown(),
|
searchPage: SearchPageSchema,
|
||||||
});
|
askAi: AskAiSchema.optional(),
|
||||||
|
sidePanel: Joi.alternatives().try(Joi.boolean(), SidePanelSchema).optional(),
|
||||||
|
})
|
||||||
|
.label('themeConfig.docsearch')
|
||||||
|
.unknown(false);
|
||||||
|
|
||||||
function ensureSidepanelSupported(themeConfig: ThemeConfig) {
|
const Schema = Joi.object<ThemeConfig>({
|
||||||
const docsearch = getDocSearchConfig(themeConfig);
|
docsearch: DocSearchSchema.required(),
|
||||||
const sidePanelEnabled = docsearch.askAi && typeof docsearch.askAi === 'object' && Boolean(docsearch.askAi.sidePanel);
|
}).unknown(false);
|
||||||
|
|
||||||
if (!sidePanelEnabled) {
|
function assertNoRemovedKeys(themeConfig: ThemeConfig): void {
|
||||||
|
const themeConfigRecord = themeConfig as Record<string, unknown>;
|
||||||
|
|
||||||
|
if (themeConfigRecord.algolia !== undefined) {
|
||||||
|
throw new Error(
|
||||||
|
'`themeConfig.algolia` is no longer supported by @docsearch/docusaurus-adapter v5. Move the configuration to `themeConfig.docsearch`.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const docsearch = themeConfigRecord.docsearch;
|
||||||
|
if (!docsearch || typeof docsearch !== 'object') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSidepanelSupported = (() => {
|
const docsearchRecord = docsearch as Record<string, unknown>;
|
||||||
const match = docSearchVersionString.match(/^(?<major>\d+)\.(?<minor>\d+)/);
|
|
||||||
if (!match?.groups) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const major = Number(match.groups.major);
|
|
||||||
const minor = Number(match.groups.minor);
|
|
||||||
return major > 4 || (major === 4 && minor >= 5);
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (!isSidepanelSupported) {
|
if (docsearchRecord.indexName !== undefined) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'The askAi.sidePanel feature is only supported in DocSearch v4.5+. ' +
|
'`themeConfig.docsearch.indexName` was removed. Use `themeConfig.docsearch.indices` instead.'
|
||||||
'Please upgrade to DocSearch v4.5+ or remove the askAi.sidePanel configuration.',
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (docsearchRecord.searchParameters !== undefined) {
|
||||||
|
throw new Error(
|
||||||
|
'`themeConfig.docsearch.searchParameters` was removed. Configure `searchParameters` on each `themeConfig.docsearch.indices` entry instead.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (docsearchRecord.searchPagePath !== undefined) {
|
||||||
|
throw new Error(
|
||||||
|
'`themeConfig.docsearch.searchPagePath` was removed. Use `themeConfig.docsearch.searchPage` instead.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sidePanel = docsearchRecord.sidePanel;
|
||||||
|
if (
|
||||||
|
sidePanel &&
|
||||||
|
typeof sidePanel === 'object' &&
|
||||||
|
(sidePanel as Record<string, unknown>).tools !== undefined
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'`themeConfig.docsearch.sidePanel.tools` is not supported because Docusaurus removes function values when serializing theme config. Pass custom tools through a swizzled `@theme/SearchBar` component instead: use `askAi` for the modal and `sidePanel` for the side panel.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const askAi = docsearchRecord.askAi;
|
||||||
|
if (typeof askAi === 'string') {
|
||||||
|
throw new Error(
|
||||||
|
'`themeConfig.docsearch.askAi` must be an object with `agentId`.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!askAi || typeof askAi !== 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const askAiRecord = askAi as Record<string, unknown>;
|
||||||
|
|
||||||
|
if (askAiRecord.agentStudio !== undefined) {
|
||||||
|
throw new Error(
|
||||||
|
'`themeConfig.docsearch.askAi.agentStudio` was removed. The adapter now only supports Agent Studio.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
askAiRecord.indexName !== undefined ||
|
||||||
|
askAiRecord.apiKey !== undefined ||
|
||||||
|
askAiRecord.appId !== undefined
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'`themeConfig.docsearch.askAi.indexName`, `apiKey`, and `appId` were removed. Use the top-level DocSearch credentials instead.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (askAiRecord.sidePanel !== undefined) {
|
||||||
|
throw new Error(
|
||||||
|
'`themeConfig.docsearch.askAi.sidePanel` was removed. Use `themeConfig.docsearch.sidePanel` instead.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (askAiRecord.tools !== undefined) {
|
||||||
|
throw new Error(
|
||||||
|
'`themeConfig.docsearch.askAi.tools` is not supported because Docusaurus removes function values when serializing theme config. Pass custom tools through a swizzled `@theme/SearchBar` component instead: use `askAi` for the modal and `sidePanel` for the side panel.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasConfigValue<TValue>(value: TValue | null | undefined): value is TValue {
|
function ensureSidePanelHasAskAi(themeConfig: ThemeConfig): void {
|
||||||
return value !== undefined && value !== null;
|
const { docsearch } = themeConfig;
|
||||||
}
|
|
||||||
|
|
||||||
function getThemeConfigSource(themeConfig: ThemeConfig): 'algolia' | 'docsearch' | null {
|
if (docsearch?.sidePanel && !docsearch.askAi) {
|
||||||
const hasDocsearch = hasConfigValue(themeConfig.docsearch);
|
|
||||||
const hasAlgolia = hasConfigValue(themeConfig.algolia);
|
|
||||||
|
|
||||||
if (hasDocsearch && hasAlgolia) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Please provide either "themeConfig.docsearch" (preferred) or "themeConfig.algolia" (legacy), but not both.',
|
'`themeConfig.docsearch.sidePanel` requires `themeConfig.docsearch.askAi`.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasDocsearch) {
|
|
||||||
return 'docsearch';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasAlgolia) {
|
|
||||||
return 'algolia';
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateThemeConfig({
|
export function validateThemeConfig({
|
||||||
validate,
|
validate,
|
||||||
themeConfig: themeConfigInput,
|
themeConfig: themeConfigInput,
|
||||||
}: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
|
}: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
|
||||||
const source = getThemeConfigSource(themeConfigInput);
|
assertNoRemovedKeys(themeConfigInput);
|
||||||
|
|
||||||
if (!source) {
|
const themeConfig = validate(Schema, {
|
||||||
return validate(Schema, {});
|
docsearch: themeConfigInput.docsearch,
|
||||||
}
|
|
||||||
|
|
||||||
const validated = validate(Schema, {
|
|
||||||
algolia: source === 'docsearch' ? themeConfigInput.docsearch : themeConfigInput.algolia,
|
|
||||||
}) as ThemeConfig;
|
}) as ThemeConfig;
|
||||||
|
|
||||||
const themeConfig: ThemeConfig = {
|
ensureSidePanelHasAskAi(themeConfig);
|
||||||
[source]: validated.algolia,
|
|
||||||
};
|
|
||||||
|
|
||||||
ensureSidepanelSupported(themeConfig);
|
|
||||||
return themeConfig;
|
return themeConfig;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,11 @@
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"composite": true
|
"composite": true
|
||||||
},
|
},
|
||||||
"include": ["src/theme", "src/client", "src/getDocSearchConfig.ts", "src/*.d.ts"],
|
"include": [
|
||||||
|
"src/theme",
|
||||||
|
"src/client",
|
||||||
|
"src/getDocSearchConfig.ts",
|
||||||
|
"src/*.d.ts"
|
||||||
|
],
|
||||||
"exclude": ["**/__tests__/**"]
|
"exclude": ["**/__tests__/**"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"references": [{"path": "./tsconfig.client.json"}],
|
"references": [{ "path": "./tsconfig.client.json" }],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"noEmit": false,
|
"noEmit": false,
|
||||||
"outDir": "lib",
|
"outDir": "lib",
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,19 @@
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"path": "packages/docsearch-css/dist/style.css",
|
"path": "packages/docsearch-css/dist/style.css",
|
||||||
"maxSize": "6 kB"
|
"maxSize": "8.2 kB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "packages/docsearch-react/dist/umd/index.js",
|
"path": "packages/docsearch-react/dist/umd/index.js",
|
||||||
"maxSize": "115 kB"
|
"maxSize": "160 kB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "packages/docsearch-js/dist/umd/index.js",
|
"path": "packages/docsearch-js/dist/umd/index.js",
|
||||||
"maxSize": "128 kB"
|
"maxSize": "180 kB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "packages/docsearch-js/dist/umd/docsearch.js",
|
||||||
|
"maxSize": "100 kB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "packages/docsearch-core/dist/umd/index.js",
|
"path": "packages/docsearch-core/dist/umd/index.js",
|
||||||
|
|
@ -19,6 +23,14 @@
|
||||||
{
|
{
|
||||||
"path": "packages/docsearch-modal/dist/umd/index.js",
|
"path": "packages/docsearch-modal/dist/umd/index.js",
|
||||||
"maxSize": "113.5 kB"
|
"maxSize": "113.5 kB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "packages/docsearch-sidepanel/dist/umd/index.js",
|
||||||
|
"maxSize": "2 kB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "packages/docsearch-sidepanel-js/dist/umd/index.js",
|
||||||
|
"maxSize": "140 kB"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import { defineConfig } from 'cypress';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
projectId: 'nf9rdc',
|
|
||||||
e2e: {
|
|
||||||
baseUrl: 'http://localhost:3000',
|
|
||||||
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Binary file not shown.
|
|
@ -1,48 +0,0 @@
|
||||||
/// <reference path="../support/commands.d.ts" />
|
|
||||||
|
|
||||||
describe.skip('Ask AI', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(Cypress.config().baseUrl!);
|
|
||||||
cy.openModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Results are displayed after a query', () => {
|
|
||||||
cy.typeQueryMatching();
|
|
||||||
cy.get('.DocSearch-Hits').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Shows Ask AI as a hit on search', () => {
|
|
||||||
cy.typeQueryMatching();
|
|
||||||
cy.get('.DocSearch-AskAi-Section').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Opens Ask AI on enter key', () => {
|
|
||||||
cy.typeQueryMatching();
|
|
||||||
cy.get('.DocSearch-Input').type('{enter}');
|
|
||||||
cy.get('.DocSearch-AskAiScreen').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Opens Ask AI on click', () => {
|
|
||||||
cy.typeQueryMatching();
|
|
||||||
cy.get('#docsearch-askAI-item-0').click();
|
|
||||||
cy.get('.DocSearch-AskAiScreen').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Streams response after query', () => {
|
|
||||||
cy.goToAskAi();
|
|
||||||
cy.get('.DocSearch-AskAiScreen-Query').should('be.visible');
|
|
||||||
cy.get('.DocSearch-AskAiScreen-Message').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Copy button copies the response to the clipboard', () => {
|
|
||||||
cy.goToAskAi();
|
|
||||||
cy.get('.DocSearch-AskAiScreen-Response').should('be.visible');
|
|
||||||
cy.window().its('navigator.permissions').invoke('query', { name: 'clipboard-read' }).its('state').then(cy.log);
|
|
||||||
cy.window()
|
|
||||||
.its('navigator.clipboard')
|
|
||||||
.invoke('readText')
|
|
||||||
.then((text) => {
|
|
||||||
expect(text.length).to.be.greaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
/// <reference path="../support/commands.d.ts" />
|
|
||||||
|
|
||||||
describe('Start', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(Cypress.config().baseUrl!);
|
|
||||||
cy.waitLoad();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Open modal on search button click', () => {
|
|
||||||
cy.openModal();
|
|
||||||
cy.modalIsVisibleAndFocused();
|
|
||||||
|
|
||||||
// check that the scrollbar offset is compensated
|
|
||||||
cy.get('body').should('have.css', 'overflow', 'hidden');
|
|
||||||
cy.get('.DocSearch-Modal').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Open modal with key shortcut on Windows/Linux', () => {
|
|
||||||
cy.get('body').type('{ctrl}k');
|
|
||||||
cy.modalIsVisibleAndFocused();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Open modal with key shortcut on Windows/Linux when caps lock is on', () => {
|
|
||||||
cy.get('body').type('{ctrl}K');
|
|
||||||
cy.modalIsVisibleAndFocused();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Open modal with key shortcut on macOS', () => {
|
|
||||||
cy.get('body').type('{meta}k');
|
|
||||||
cy.modalIsVisibleAndFocused();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Open modal with key shortcut on macOS when caps lock is on', () => {
|
|
||||||
cy.get('body').type('{meta}K');
|
|
||||||
cy.modalIsVisibleAndFocused();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Open modal with forward slash key shortcut', () => {
|
|
||||||
cy.get('body').wait(1000).type('/');
|
|
||||||
cy.modalIsVisibleAndFocused();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('End', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(Cypress.config().baseUrl!);
|
|
||||||
cy.openModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Close modal with Esc key', () => {
|
|
||||||
cy.closeModal();
|
|
||||||
cy.modalIsNotVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Close modal by clicking outside its container', () => {
|
|
||||||
cy.get('body').click(0, 0);
|
|
||||||
cy.modalIsNotVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Close modal with key shortcut on Windows/Linux', () => {
|
|
||||||
cy.get('body').type('{ctrl}k');
|
|
||||||
cy.modalIsNotVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Close modal with key shortcut on macOS', () => {
|
|
||||||
cy.get('body').type('{meta}k');
|
|
||||||
cy.modalIsNotVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Search', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(Cypress.config().baseUrl!);
|
|
||||||
cy.openModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Results are displayed after a query', () => {
|
|
||||||
cy.typeQueryMatching();
|
|
||||||
cy.get('.DocSearch-Hits').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Query can be cleared', () => {
|
|
||||||
cy.typeQueryMatching();
|
|
||||||
cy.get('.DocSearch-Clear').click();
|
|
||||||
cy.get('.DocSearch-Hits').should('not.exist');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Keyboard navigation leads to result', () => {
|
|
||||||
const currentURL = cy.url();
|
|
||||||
|
|
||||||
cy.typeQueryMatching();
|
|
||||||
cy.get('.DocSearch-Input').type('{downArrow}{downArrow}{upArrow}');
|
|
||||||
cy.get('.DocSearch-Input').type('{enter}');
|
|
||||||
cy.on('url:changed', (newUrl) => {
|
|
||||||
expect(newUrl).not.equal(currentURL);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Pointer navigation leads to result', () => {
|
|
||||||
const currentURL = cy.url();
|
|
||||||
|
|
||||||
cy.typeQueryMatching();
|
|
||||||
cy.get('#docsearch-hits_docsearch_0-item-1 > a').click({
|
|
||||||
force: true,
|
|
||||||
});
|
|
||||||
cy.on('url:changed', (newUrl) => {
|
|
||||||
expect(newUrl).not.equal(currentURL);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("No results are displayed if query doesn't match", () => {
|
|
||||||
cy.typeQueryNotMatching();
|
|
||||||
cy.contains('No results found for').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Should not refer to Recent/Favorite in aria-controls', () => {
|
|
||||||
cy.get('.DocSearch-Input').should('not.have.attr', 'aria-controls');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Recent and Favorites', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(Cypress.config().baseUrl!);
|
|
||||||
cy.openModal();
|
|
||||||
cy.typeQueryMatching();
|
|
||||||
cy.get('#docsearch-hits_docsearch_0-item-1 > a').click({ force: true }).wait(1000);
|
|
||||||
cy.openModal();
|
|
||||||
cy.contains('Recent').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Recent search is displayed after visiting a result', () => {
|
|
||||||
cy.clearSearch();
|
|
||||||
cy.get('#docsearch-recentSearches-item-0').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Recent search can be deleted', () => {
|
|
||||||
cy.get('#docsearch-recentSearches-item-0').find('[title="Remove this search from history"]').trigger('click');
|
|
||||||
cy.get('.DocSearch-Hits').should('not.exist');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Recent search can be favorited', () => {
|
|
||||||
cy.get('#docsearch-recentSearches-item-0').find('[title="Save this search"]').trigger('click');
|
|
||||||
cy.contains('Favorite').should('be.visible');
|
|
||||||
cy.get('#docsearch-favoriteSearches-item-0').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Favorite can be deleted', () => {
|
|
||||||
cy.get('#docsearch-recentSearches-item-0').find('[title="Save this search"]').trigger('click');
|
|
||||||
cy.contains('Favorite').should('be.visible');
|
|
||||||
cy.get('#docsearch-favoriteSearches-item-0').find('[title="Remove this search from favorites"]').trigger('click');
|
|
||||||
cy.get('.DocSearch-Hits').should('not.exist');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
55
cypress/support/commands.d.ts
vendored
55
cypress/support/commands.d.ts
vendored
|
|
@ -1,55 +0,0 @@
|
||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
declare namespace Cypress {
|
|
||||||
interface Chainable {
|
|
||||||
/**
|
|
||||||
* Ensures the modal is visible and focused.
|
|
||||||
*/
|
|
||||||
modalIsVisibleAndFocused: () => void;
|
|
||||||
/**
|
|
||||||
* Ensures the modal not visible.
|
|
||||||
*/
|
|
||||||
modalIsNotVisible: () => void;
|
|
||||||
/**
|
|
||||||
* Toggles the dark mode on the preview website.
|
|
||||||
*/
|
|
||||||
darkmode: () => void;
|
|
||||||
/**
|
|
||||||
* Wait for the page to load.
|
|
||||||
*/
|
|
||||||
waitLoad: () => void;
|
|
||||||
/**
|
|
||||||
* Opens the DocSearch modal.
|
|
||||||
*/
|
|
||||||
openModal: () => void;
|
|
||||||
/**
|
|
||||||
* Closes the DocSearch modal.
|
|
||||||
*/
|
|
||||||
closeModal: () => void;
|
|
||||||
/**
|
|
||||||
* Search for a given query.
|
|
||||||
*/
|
|
||||||
search: (query: string) => void;
|
|
||||||
/**
|
|
||||||
* Types a query that returns results.
|
|
||||||
*/
|
|
||||||
typeQueryMatching: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the search input.
|
|
||||||
*/
|
|
||||||
clearSearch: () => void;
|
|
||||||
/**
|
|
||||||
* Types a query that returns no results.
|
|
||||||
*/
|
|
||||||
typeQueryNotMatching: () => void;
|
|
||||||
/**
|
|
||||||
* Returns to the keyword search screen.
|
|
||||||
*/
|
|
||||||
returnToSearch: () => void;
|
|
||||||
/**
|
|
||||||
* Navigates to the Ask AI screen.
|
|
||||||
*/
|
|
||||||
goToAskAi: () => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
Cypress.Commands.add('modalIsVisibleAndFocused', () => {
|
|
||||||
cy.get('.DocSearch-Modal', { timeout: 10000 }).should('be.visible');
|
|
||||||
cy.get('.DocSearch-Input').should('be.focus');
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('modalIsNotVisible', () => {
|
|
||||||
cy.get('body').should('not.have.class', 'DocSearch--active');
|
|
||||||
cy.get('.DocSearch-Modal').should('not.exist');
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('darkmode', () => {
|
|
||||||
cy.get('.react-toggle').click({ force: true });
|
|
||||||
cy.get('.react-toggle-screenreader-only').blur();
|
|
||||||
cy.get('html.dark').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('waitLoad', () => {
|
|
||||||
cy.get('.DocSearch-Button', { timeout: 10000 }).should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('openModal', () => {
|
|
||||||
cy.get('.DocSearch-Button').should('be.visible').click();
|
|
||||||
cy.modalIsVisibleAndFocused();
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('closeModal', () => {
|
|
||||||
cy.get('body').type('{esc}');
|
|
||||||
cy.modalIsNotVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('search', (query: string) => {
|
|
||||||
cy.get('.DocSearch-Input').should('be.visible').type(query);
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('typeQueryMatching', () => {
|
|
||||||
cy.search('g');
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('typeQueryNotMatching', () => {
|
|
||||||
cy.search('zzz');
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('clearSearch', () => {
|
|
||||||
cy.get('.DocSearch-Input').clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('returnToSearch', () => {
|
|
||||||
cy.get('.DocSearch-Input').type('{esc}');
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('goToAskAi', () => {
|
|
||||||
cy.typeQueryMatching();
|
|
||||||
cy.get('#docsearch-AskAi-Section').click();
|
|
||||||
});
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
// ***********************************************************
|
|
||||||
// This example support/index.js is processed and
|
|
||||||
// loaded automatically before your test files.
|
|
||||||
//
|
|
||||||
// This is a great place to put global configuration and
|
|
||||||
// behavior that modifies Cypress.
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off
|
|
||||||
// automatically serving support files with the
|
|
||||||
// 'supportFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/configuration
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
|
||||||
import './commands';
|
|
||||||
|
|
||||||
// Alternatively you can use CommonJS syntax:
|
|
||||||
// require('./commands')
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"types": ["cypress"]
|
|
||||||
},
|
|
||||||
"exclude": []
|
|
||||||
}
|
|
||||||
71
e2e/a11y.test.ts
Normal file
71
e2e/a11y.test.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { test, expect, gatherA11yViolations } from './fixtures';
|
||||||
|
|
||||||
|
test.describe('a11y > Modal', () => {
|
||||||
|
test.beforeEach(async ({ docSearch }) => {
|
||||||
|
await docSearch.goto();
|
||||||
|
await docSearch.waitForLoad();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Smoke test', async ({ docSearch, axe }, testInfo) => {
|
||||||
|
await docSearch.openModal();
|
||||||
|
|
||||||
|
const scanResults = await axe().include('.DocSearch-Container').analyze();
|
||||||
|
|
||||||
|
await testInfo.attach('a11y-scan-results-modal', {
|
||||||
|
body: JSON.stringify(scanResults.violations, null, 2),
|
||||||
|
contentType: 'application/json',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6 is the current number of reported violations
|
||||||
|
expect(
|
||||||
|
gatherA11yViolations(scanResults.violations).length
|
||||||
|
).toBeLessThanOrEqual(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Search results', async ({ docSearch, axe }, testInfo) => {
|
||||||
|
await docSearch.openModal();
|
||||||
|
|
||||||
|
await docSearch.typeQueryMatching();
|
||||||
|
|
||||||
|
await expect(docSearch.hits).toBeVisible();
|
||||||
|
|
||||||
|
const scanResults = await axe()
|
||||||
|
.include('#docsearch-hits_docsearch_0-list')
|
||||||
|
.analyze();
|
||||||
|
|
||||||
|
await testInfo.attach('a11y-scan-results-modal-search-results', {
|
||||||
|
body: JSON.stringify(scanResults.violations, null, 2),
|
||||||
|
contentType: 'application/json',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 24 is the current number of reported violations
|
||||||
|
expect(
|
||||||
|
gatherA11yViolations(scanResults.violations).length
|
||||||
|
).toBeLessThanOrEqual(24);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('a11y > Sidepanel', () => {
|
||||||
|
test.beforeEach(async ({ docSearch, sidepanel }) => {
|
||||||
|
await docSearch.goto();
|
||||||
|
await sidepanel.waitForLoad();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Smoke test', async ({ sidepanel, axe }, testInfo) => {
|
||||||
|
await sidepanel.openSidepanel();
|
||||||
|
|
||||||
|
const scanResults = await axe()
|
||||||
|
.include('.DocSearch-Sidepanel-Container')
|
||||||
|
.analyze();
|
||||||
|
|
||||||
|
await testInfo.attach('a11y-scan-results-sidepanel', {
|
||||||
|
body: JSON.stringify(scanResults.violations, null, 2),
|
||||||
|
contentType: 'application/json',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4 is the current number of reported violations
|
||||||
|
expect(
|
||||||
|
gatherA11yViolations(scanResults.violations).length
|
||||||
|
).toBeLessThanOrEqual(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
53
e2e/askai.spec.ts
Normal file
53
e2e/askai.spec.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { test, expect } from './fixtures';
|
||||||
|
|
||||||
|
test.describe.skip('Ask AI', () => {
|
||||||
|
test.beforeEach(async ({ docSearch }) => {
|
||||||
|
await docSearch.goto();
|
||||||
|
await docSearch.openModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Results are displayed after a query', async ({ docSearch }) => {
|
||||||
|
await docSearch.typeQueryMatching();
|
||||||
|
await expect(docSearch.hits).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Shows Ask AI as a hit on search', async ({ docSearch, page }) => {
|
||||||
|
await docSearch.typeQueryMatching();
|
||||||
|
await expect(page.locator('.DocSearch-AskAi-Section')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Opens Ask AI on enter key', async ({ docSearch, page }) => {
|
||||||
|
await docSearch.typeQueryMatching();
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await expect(page.locator('.DocSearch-AskAiScreen')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Opens Ask AI on click', async ({ docSearch, page }) => {
|
||||||
|
await docSearch.typeQueryMatching();
|
||||||
|
await page.locator('#docsearch-askAI-item-0').click();
|
||||||
|
await expect(page.locator('.DocSearch-AskAiScreen')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Streams response after query', async ({ docSearch, page }) => {
|
||||||
|
await docSearch.goToAskAi();
|
||||||
|
await expect(page.locator('.DocSearch-AskAiScreen-Query')).toBeVisible();
|
||||||
|
await expect(page.locator('.DocSearch-AskAiScreen-Message')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Copy button copies the response to the clipboard', async ({
|
||||||
|
docSearch,
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
}) => {
|
||||||
|
// Grant clipboard permissions
|
||||||
|
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
|
||||||
|
|
||||||
|
await docSearch.goToAskAi();
|
||||||
|
await expect(page.locator('.DocSearch-AskAiScreen-Response')).toBeVisible();
|
||||||
|
|
||||||
|
const clipboardText = await page.evaluate(() =>
|
||||||
|
navigator.clipboard.readText()
|
||||||
|
);
|
||||||
|
expect(clipboardText.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
150
e2e/fixtures.ts
Normal file
150
e2e/fixtures.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
// oxlint-disable max-classes-per-file react-hooks/rules-of-hooks
|
||||||
|
import AxeBuilder from '@axe-core/playwright';
|
||||||
|
import {
|
||||||
|
test as base,
|
||||||
|
expect,
|
||||||
|
type Page,
|
||||||
|
type Locator,
|
||||||
|
} from '@playwright/test';
|
||||||
|
|
||||||
|
export class DocSearchPage {
|
||||||
|
readonly page: Page;
|
||||||
|
readonly searchButton: Locator;
|
||||||
|
readonly modal: Locator;
|
||||||
|
readonly input: Locator;
|
||||||
|
readonly hits: Locator;
|
||||||
|
readonly clearButton: Locator;
|
||||||
|
readonly firstHit: Locator;
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
this.page = page;
|
||||||
|
this.searchButton = page.locator('.DocSearch-Button');
|
||||||
|
this.modal = page.locator('.DocSearch-Modal');
|
||||||
|
this.input = page.locator('.DocSearch-Input');
|
||||||
|
this.hits = page.locator('.DocSearch-Hits').first();
|
||||||
|
this.clearButton = page.locator('.DocSearch-Clear');
|
||||||
|
this.firstHit = page
|
||||||
|
.locator('#docsearch-hits_docsearch_0-list .DocSearch-Hit a')
|
||||||
|
.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
async goto(): Promise<void> {
|
||||||
|
await this.page.goto('/docs/what-is-docsearch');
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForLoad(): Promise<void> {
|
||||||
|
await expect(this.searchButton).toBeVisible({ timeout: 10000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async openModal(): Promise<void> {
|
||||||
|
await this.searchButton.click();
|
||||||
|
await this.expectModalVisibleAndFocused();
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeModal(): Promise<void> {
|
||||||
|
await this.page.keyboard.press('Escape');
|
||||||
|
await this.expectModalNotVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
async expectModalVisibleAndFocused(): Promise<void> {
|
||||||
|
await expect(this.modal).toBeVisible({ timeout: 10000 });
|
||||||
|
await expect(this.input).toBeFocused();
|
||||||
|
}
|
||||||
|
|
||||||
|
async expectModalNotVisible(): Promise<void> {
|
||||||
|
await expect(this.page.locator('body')).not.toHaveClass(
|
||||||
|
/DocSearch--active/
|
||||||
|
);
|
||||||
|
await expect(this.modal).not.toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(query: string): Promise<void> {
|
||||||
|
await this.input.fill(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
async typeQueryMatching(): Promise<void> {
|
||||||
|
await this.search('g');
|
||||||
|
}
|
||||||
|
|
||||||
|
async typeQueryNotMatching(): Promise<void> {
|
||||||
|
await this.search('zzz');
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearSearch(): Promise<void> {
|
||||||
|
await this.input.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickFirstHit(): Promise<void> {
|
||||||
|
await expect(this.hits).toBeVisible();
|
||||||
|
await this.firstHit.click({ force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async enableDarkMode(): Promise<void> {
|
||||||
|
await this.page.locator('.react-toggle').click({ force: true });
|
||||||
|
await this.page.locator('.react-toggle-screenreader-only').blur();
|
||||||
|
await expect(this.page.locator('html.dark')).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
async goToAskAi(): Promise<void> {
|
||||||
|
await this.typeQueryMatching();
|
||||||
|
await this.page.locator('#docsearch-AskAi-Section').click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SidepanelPage {
|
||||||
|
readonly page: Page;
|
||||||
|
readonly sidepanelButton: Locator;
|
||||||
|
readonly sidepanel: Locator;
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
this.page = page;
|
||||||
|
this.sidepanelButton = page.locator('.DocSearch-SidepanelButton');
|
||||||
|
this.sidepanel = page.locator('.DocSearch-Sidepanel-Container');
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForLoad(): Promise<void> {
|
||||||
|
await expect(this.sidepanelButton).toBeVisible({ timeout: 10000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async openSidepanel(): Promise<void> {
|
||||||
|
await this.sidepanelButton.click();
|
||||||
|
await expect(this.sidepanel).toHaveClass(/is-open/, {
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const test = base.extend<{
|
||||||
|
docSearch: DocSearchPage;
|
||||||
|
sidepanel: SidepanelPage;
|
||||||
|
axe: () => AxeBuilder;
|
||||||
|
}>({
|
||||||
|
docSearch: async ({ page }, use) => {
|
||||||
|
const docSearch = new DocSearchPage(page);
|
||||||
|
await use(docSearch);
|
||||||
|
},
|
||||||
|
sidepanel: async ({ page }, use) => {
|
||||||
|
const sidepanel = new SidepanelPage(page);
|
||||||
|
await use(sidepanel);
|
||||||
|
},
|
||||||
|
axe: async ({ page }, use) => {
|
||||||
|
const makeBuilder = () =>
|
||||||
|
new AxeBuilder({ page }).withTags([
|
||||||
|
'wcag2a',
|
||||||
|
'wcag2aa',
|
||||||
|
'wcag21a',
|
||||||
|
'wcag21aa',
|
||||||
|
'wcag22a',
|
||||||
|
'wcag22aa',
|
||||||
|
]);
|
||||||
|
await use(makeBuilder);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type AxeScanResults = Awaited<ReturnType<AxeBuilder['analyze']>>;
|
||||||
|
|
||||||
|
function gatherA11yViolations(violations: AxeScanResults['violations']) {
|
||||||
|
return violations.flatMap((v) => v.nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { expect, gatherA11yViolations };
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue