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
|
||||
|
||||
aliases:
|
||||
- &restore_yarn_cache
|
||||
name: Restore Yarn cache
|
||||
- &install_bun
|
||||
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:
|
||||
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||
- bun-packages-{{ checksum "bun.lock" }}
|
||||
|
||||
- &save_yarn_cache
|
||||
name: Save Yarn cache
|
||||
key: yarn-packages-{{ checksum "yarn.lock" }}
|
||||
- &save_bun_cache
|
||||
name: Save Bun cache
|
||||
key: bun-packages-{{ checksum "bun.lock" }}
|
||||
paths:
|
||||
- .yarn/cache
|
||||
- .yarn/unplugged
|
||||
- ~/.bun/install/cache
|
||||
|
||||
- &run_yarn_install
|
||||
- &run_bun_install
|
||||
name: Install dependencies
|
||||
command: yarn install --immutable
|
||||
command: bun install --frozen-lockfile
|
||||
|
||||
- &restore_dist_folders
|
||||
name: Restore dist folders
|
||||
|
|
@ -29,6 +34,8 @@ aliases:
|
|||
mkdir -p packages/docsearch-modal/dist
|
||||
mkdir -p packages/docsearch-sidepanel/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-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-sidepanel/dist packages/docsearch-sidepanel
|
||||
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
|
||||
working_directory: ~/docsearch
|
||||
docker:
|
||||
- image: cimg/node:22.15.0
|
||||
- image: cimg/node:24.13.1
|
||||
|
||||
cypress: &cypress
|
||||
playwright: &playwright
|
||||
working_directory: ~/docsearch
|
||||
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:
|
||||
## this enables colors in the output
|
||||
TERM: xterm
|
||||
|
|
@ -62,13 +71,15 @@ jobs:
|
|||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache: *restore_yarn_cache
|
||||
- run: *run_yarn_install
|
||||
- save_cache: *save_yarn_cache
|
||||
- run: *install_bun
|
||||
- restore_cache: *restore_bun_cache
|
||||
- run: *run_bun_install
|
||||
- save_cache: *save_bun_cache
|
||||
- run:
|
||||
name: Build and Size
|
||||
command: |
|
||||
yarn run build
|
||||
bun run build
|
||||
bun run --filter @docsearch/cli test:pack
|
||||
- run:
|
||||
name: Move dist folders to workspace
|
||||
command: |
|
||||
|
|
@ -81,6 +92,8 @@ jobs:
|
|||
mkdir -p /tmp/workspace/packages/docsearch-modal/dist
|
||||
mkdir -p /tmp/workspace/packages/docsearch-sidepanel/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-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-sidepanel/dist /tmp/workspace/packages/docsearch-sidepanel
|
||||
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:
|
||||
root: *workspace_root
|
||||
paths:
|
||||
- packages
|
||||
- adapters
|
||||
test_lint:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- *attach_workspace
|
||||
- restore_cache: *restore_yarn_cache
|
||||
- run: *run_yarn_install
|
||||
- save_cache: *save_yarn_cache
|
||||
- run: *install_bun
|
||||
- restore_cache: *restore_bun_cache
|
||||
- run: *run_bun_install
|
||||
- save_cache: *save_bun_cache
|
||||
- run: *restore_dist_folders
|
||||
- run:
|
||||
name: Linting
|
||||
command: yarn run lint
|
||||
command: |
|
||||
bun run lint --quiet --format=stylish
|
||||
bun run lint:css
|
||||
test_types:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- *attach_workspace
|
||||
- restore_cache: *restore_yarn_cache
|
||||
- run: *run_yarn_install
|
||||
- save_cache: *save_yarn_cache
|
||||
- run: *install_bun
|
||||
- restore_cache: *restore_bun_cache
|
||||
- run: *run_bun_install
|
||||
- save_cache: *save_bun_cache
|
||||
- run: *restore_dist_folders
|
||||
- run:
|
||||
name: Type checking
|
||||
command: yarn run test:types
|
||||
command: bun run test:types
|
||||
test_size:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- *attach_workspace
|
||||
- restore_cache: *restore_yarn_cache
|
||||
- run: *run_yarn_install
|
||||
- save_cache: *save_yarn_cache
|
||||
- run: *install_bun
|
||||
- restore_cache: *restore_bun_cache
|
||||
- run: *run_bun_install
|
||||
- save_cache: *save_bun_cache
|
||||
- run: *restore_dist_folders
|
||||
- run:
|
||||
name: Size checking
|
||||
command: yarn run test:size
|
||||
command: bun run test:size
|
||||
test_unit:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- *attach_workspace
|
||||
- restore_cache: *restore_yarn_cache
|
||||
- run: *run_yarn_install
|
||||
- save_cache: *save_yarn_cache
|
||||
- run: *install_bun
|
||||
- restore_cache: *restore_bun_cache
|
||||
- run: *run_bun_install
|
||||
- save_cache: *save_bun_cache
|
||||
- run: *restore_dist_folders
|
||||
- run:
|
||||
name: Unit tests
|
||||
command: yarn run test
|
||||
command: bun run test
|
||||
release:
|
||||
<<: *defaults
|
||||
working_directory: ~/docsearch
|
||||
docker:
|
||||
- image: cimg/node:24.16
|
||||
steps:
|
||||
- checkout
|
||||
- *attach_workspace
|
||||
- restore_cache: *restore_yarn_cache
|
||||
- run: *run_yarn_install
|
||||
- save_cache: *save_yarn_cache
|
||||
- run: *install_bun
|
||||
- restore_cache: *restore_bun_cache
|
||||
- run: *run_bun_install
|
||||
- save_cache: *save_bun_cache
|
||||
- run: *restore_dist_folders
|
||||
- run:
|
||||
name: Setup git user
|
||||
|
|
@ -157,19 +182,29 @@ jobs:
|
|||
git config --global user.name "algolia-bot"
|
||||
- run:
|
||||
name: Release if needed
|
||||
command: yarn run shipjs trigger
|
||||
test_cypress:
|
||||
<<: *cypress
|
||||
command: |
|
||||
export NPM_ID_TOKEN="$(circleci run oidc get --claims '{"aud": "npm:registry.npmjs.org"}')"
|
||||
bun run release
|
||||
git push --follow-tags origin "HEAD:${CIRCLE_BRANCH}"
|
||||
test_playwright:
|
||||
<<: *playwright
|
||||
steps:
|
||||
- checkout
|
||||
- *attach_workspace
|
||||
- restore_cache: *restore_yarn_cache
|
||||
- run: *run_yarn_install
|
||||
- save_cache: *save_yarn_cache
|
||||
- run: *install_bun
|
||||
- restore_cache: *restore_bun_cache
|
||||
- run: *run_bun_install
|
||||
- save_cache: *save_bun_cache
|
||||
- run: *restore_dist_folders
|
||||
- run:
|
||||
name: Cypress test Actions
|
||||
command: yarn run cy:run
|
||||
name: Playwright E2E tests
|
||||
command: bun run pw:run
|
||||
- store_artifacts:
|
||||
path: playwright-report
|
||||
destination: playwright-report
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
destination: test-results
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
|
@ -188,7 +223,7 @@ workflows:
|
|||
- test_unit:
|
||||
requires:
|
||||
- build
|
||||
- test_cypress:
|
||||
- test_playwright:
|
||||
requires:
|
||||
- build
|
||||
- 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=
|
||||
NPM_AUTH_TOKEN=
|
||||
CYPRESS_RECORD_KEY=
|
||||
PERCY_TOKEN=
|
||||
PERCY_TOKEN=
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -21,9 +21,9 @@ dist/
|
|||
# Environment files
|
||||
.env
|
||||
|
||||
# Cypress Video and Screenshots output
|
||||
cypress/screenshots/
|
||||
cypress/videos/
|
||||
# Playwright output
|
||||
playwright-report/
|
||||
test-results/
|
||||
|
||||
**/.yarn/*
|
||||
!**/.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
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -21,98 +21,72 @@ DocSearch is an Algolia-powered search widget for documentation sites. It's a Ty
|
|||
|
||||
```bash
|
||||
# Install dependencies
|
||||
yarn install
|
||||
bun install
|
||||
|
||||
# Build all packages
|
||||
yarn build
|
||||
bun run build
|
||||
|
||||
# Build specific package
|
||||
yarn workspace @docsearch/react build
|
||||
bun run --filter @docsearch/react build
|
||||
|
||||
# Watch mode (all packages)
|
||||
yarn watch
|
||||
|
||||
# Clean builds
|
||||
yarn build:clean
|
||||
bun run watch
|
||||
```
|
||||
|
||||
## Test Commands
|
||||
|
||||
```bash
|
||||
# Run all unit tests
|
||||
yarn test
|
||||
bun run test
|
||||
|
||||
# Run a single test file
|
||||
yarn test 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
|
||||
bun run test --run packages/docsearch-react/src/__tests__/utils.test.ts
|
||||
|
||||
# Type checking
|
||||
yarn test:types
|
||||
bun run test:types
|
||||
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# Run ESLint
|
||||
yarn lint
|
||||
# Run oxlint
|
||||
bun run lint --format=agent
|
||||
|
||||
# Run CSS linting
|
||||
yarn lint:css
|
||||
# Perform oxfmt formatting
|
||||
bun run fmt
|
||||
|
||||
# Lint CSS
|
||||
bun run lint:css
|
||||
```
|
||||
|
||||
## E2E Testing (Cypress)
|
||||
## E2E Testing (Playwright)
|
||||
|
||||
```bash
|
||||
# Run Cypress tests
|
||||
yarn cy:run
|
||||
bun run pw:run
|
||||
|
||||
# Run with specific browser
|
||||
yarn cy:run:chrome
|
||||
yarn cy:run:firefox
|
||||
bun run pw:run:chromium
|
||||
bun run pw:run:firefox
|
||||
bun run pw:run:webkit
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
- Use `type` imports for type-only imports: `import type { Foo } from './types'`
|
||||
- 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
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
export declare type DocSearchHit = {
|
||||
export type DocSearchHit = {
|
||||
objectID: string;
|
||||
content: string | null;
|
||||
};
|
||||
|
|
@ -130,7 +104,7 @@ function createStorage<TItem>(key: string): StorageInterface<TItem> {
|
|||
- **Utilities**: camelCase (`removeHighlightTags.ts`)
|
||||
- **Types**: PascalCase (`InternalDocSearchHit`)
|
||||
- **Constants**: SCREAMING_SNAKE_CASE (`MAX_QUERY_SIZE`)
|
||||
- **CSS classes**: `DocSearch-` prefix with PascalCase (`DocSearch-Modal`)
|
||||
- **CSS classes**: `DocSearch-` prefix
|
||||
|
||||
### React Components
|
||||
|
||||
|
|
@ -140,6 +114,14 @@ function createStorage<TItem>(key: string): StorageInterface<TItem> {
|
|||
- Use `React.useMemo` for expensive computations
|
||||
- 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
|
||||
function DocSearchComponent(
|
||||
props: DocSearchProps,
|
||||
|
|
@ -169,7 +151,7 @@ try {
|
|||
}
|
||||
```
|
||||
|
||||
### Formatting (Prettier)
|
||||
### Formatting (oxfmt)
|
||||
|
||||
- Single quotes for strings
|
||||
- Trailing commas (ES5 style)
|
||||
|
|
@ -226,6 +208,8 @@ describe('ComponentName', () => {
|
|||
packages/
|
||||
docsearch-react/
|
||||
src/
|
||||
components/ # Scoped React composition components
|
||||
ui/ # Reusable rendering components and explicitly scoped UI pieces
|
||||
__tests__/ # Test files
|
||||
icons/ # Icon components
|
||||
types/ # Type definitions
|
||||
|
|
@ -243,3 +227,17 @@ packages/
|
|||
- `marked` - Markdown rendering
|
||||
- `rollup` - Build bundling
|
||||
- `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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# This file list code owners for the repository, or part of it.
|
||||
# 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:
|
||||
|
||||
- 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
|
||||
|
||||
```sh
|
||||
yarn run release
|
||||
```
|
||||
When changesets are merged to `main`, a "Version Packages" PR will be automatically created (or updated). Merging that PR triggers the release workflow:
|
||||
|
||||
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
|
||||
|
||||
```sh
|
||||
yarn add @docsearch/js@4
|
||||
bun add @docsearch/js@4
|
||||
# or
|
||||
npm install @docsearch/js@4
|
||||
```
|
||||
|
|
@ -64,7 +64,7 @@ import '@docsearch/css';
|
|||
docsearch({
|
||||
container: '#docsearch',
|
||||
appId: 'YOUR_APP_ID',
|
||||
indexName: 'YOUR_INDEX_NAME',
|
||||
indices: ['YOUR_INDEX_NAME'],
|
||||
apiKey: 'YOUR_SEARCH_API_KEY',
|
||||
});
|
||||
```
|
||||
|
|
@ -74,7 +74,7 @@ docsearch({
|
|||
#### Installation
|
||||
|
||||
```bash
|
||||
yarn add @docsearch/react@4
|
||||
bun add @docsearch/react@4
|
||||
# or
|
||||
npm install @docsearch/react@4
|
||||
```
|
||||
|
|
@ -111,6 +111,10 @@ export default App;
|
|||
|
||||
[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
|
||||
|
||||
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: {
|
||||
appId: 'APP_ID',
|
||||
apiKey: 'SEARCH_API_KEY',
|
||||
indexName: 'INDEX_NAME',
|
||||
indices: [{ name: 'INDEX_NAME' }],
|
||||
askAi: {
|
||||
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",
|
||||
"version": "4.7.0",
|
||||
"version": "5.0.0-beta.4",
|
||||
"description": "Algolia search component for Docusaurus.",
|
||||
"main": "lib/index.js",
|
||||
"sideEffects": [
|
||||
|
|
@ -27,35 +27,39 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "yarn exec tsc --build --force && node ./scripts/copy-assets.mjs && node ./scripts/format-theme.mjs",
|
||||
"build:clean": "yarn clean && yarn build",
|
||||
"build": "bun x tsc --build --force && node ./scripts/copy-assets.mjs && oxfmt --config ../../.oxfmtrc.json ./lib/theme/**/*",
|
||||
"build:clean": "bun run clean && bun run build",
|
||||
"clean": "rm -rf lib tsconfig.*.tsbuildinfo",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docsearch/react": "4.7.0",
|
||||
"@docusaurus/core": "^3.9.2",
|
||||
"@docusaurus/plugin-content-docs": "^3.9.2",
|
||||
"@docusaurus/theme-common": "^3.9.2",
|
||||
"@docusaurus/theme-translations": "^3.9.2",
|
||||
"@algolia/autocomplete-core": "1.19.2",
|
||||
"@docsearch/core": "workspace:*",
|
||||
"@docsearch/modal": "workspace:*",
|
||||
"@docsearch/react": "workspace:*",
|
||||
"@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-helper": "^3.26.0",
|
||||
"clsx": "^2.0.0",
|
||||
"eta": "^2.2.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"joi": "^17.9.2",
|
||||
"lodash": "^4.17.21",
|
||||
"tslib": "^2.6.0",
|
||||
"utility-types": "^3.10.0"
|
||||
"search-insights": "^2.17.3",
|
||||
"tslib": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/core": "^3.9.2",
|
||||
"@docusaurus/module-type-aliases": "3.9.2",
|
||||
"@docusaurus/theme-classic": "3.9.2",
|
||||
"@docusaurus/core": "^3.10.2",
|
||||
"@docusaurus/module-type-aliases": "3.10.2",
|
||||
"@docusaurus/theme-classic": "3.10.2",
|
||||
"@docusaurus/types": "^3.10.2",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash": "^4.17.10",
|
||||
"glob": "^13.0.6",
|
||||
"typescript": "5.7.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ async function copyAssetsOnce() {
|
|||
}
|
||||
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.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
|
|
@ -17,7 +17,11 @@ describe('mergeFacetFilters', () => {
|
|||
});
|
||||
|
||||
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]', () => {
|
||||
|
|
@ -29,16 +33,27 @@ describe('mergeFacetFilters', () => {
|
|||
});
|
||||
|
||||
it('merges [array,undefined]', () => {
|
||||
assert.deepStrictEqual(mergeFacetFilters(['f1', 'f2'], undefined), ['f1', 'f2']);
|
||||
assert.deepStrictEqual(mergeFacetFilters(['f1', 'f2'], undefined), [
|
||||
'f1',
|
||||
'f2',
|
||||
]);
|
||||
});
|
||||
|
||||
it('merges [undefined,array]', () => {
|
||||
assert.deepStrictEqual(mergeFacetFilters(undefined, ['f1', 'f2']), ['f1', 'f2']);
|
||||
assert.deepStrictEqual(mergeFacetFilters(undefined, ['f1', 'f2']), [
|
||||
'f1',
|
||||
'f2',
|
||||
]);
|
||||
});
|
||||
|
||||
it('merges [array,array]', () => {
|
||||
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.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
export { useAlgoliaThemeConfig } from './useAlgoliaThemeConfig';
|
||||
|
|
|
|||
|
|
@ -1,121 +1,112 @@
|
|||
/**
|
||||
* 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.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import type { AskAiConfig } from '@docsearch/docusaurus-adapter';
|
||||
import type { DocSearchModalProps, DocSearchTranslations } from '@docsearch/react';
|
||||
import translations from '@theme/SearchTranslations';
|
||||
import type { DocSearchAskAi, DocSearchProps } from '@docsearch/react';
|
||||
import type { FacetFilters } from 'algoliasearch/lite';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useAlgoliaContextualFacetFiltersIfEnabled } from './useAlgoliaContextualFacetFilters';
|
||||
import { mergeFacetFilters } from './utils';
|
||||
|
||||
type AskAiOptions = AskAiConfig & Pick<DocSearchAskAi, 'tools'>;
|
||||
// The minimal props the hook needs from DocSearch
|
||||
interface DocSearchPropsLite {
|
||||
indexName: string;
|
||||
apiKey: string;
|
||||
appId: string;
|
||||
placeholder?: string;
|
||||
translations?: DocSearchTranslations;
|
||||
searchParameters?: DocSearchModalProps['searchParameters'];
|
||||
askAi?: AskAiConfig;
|
||||
indices: NonNullable<DocSearchProps['indices']>;
|
||||
askAi?: AskAiOptions;
|
||||
}
|
||||
|
||||
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 = {
|
||||
canHandleAskAi: boolean;
|
||||
isAskAiActive: boolean;
|
||||
currentPlaceholder: string | undefined;
|
||||
onAskAiToggle: OnAskAiToggle;
|
||||
askAi?: AskAiConfig;
|
||||
extraAskAiProps: DocSearchModalPropsLite & {
|
||||
askAi?: DocSearchAskAi;
|
||||
canHandleAskAi?: boolean;
|
||||
isAskAiActive?: boolean;
|
||||
onAskAiToggle?: OnAskAiToggle;
|
||||
modalAskAi?: DocSearchAskAi;
|
||||
sidePanelAskAi?: DocSearchAskAi & {
|
||||
apiKey: string;
|
||||
appId: string;
|
||||
indexName: string;
|
||||
};
|
||||
};
|
||||
|
||||
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
|
||||
// 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(
|
||||
askAi: AskAiConfig | undefined,
|
||||
contextualSearchFilters: FacetFilters | undefined,
|
||||
): AskAiConfig | undefined {
|
||||
if (!askAi) {
|
||||
return undefined;
|
||||
}
|
||||
if (askAi.agentStudio === true) {
|
||||
askAi: AskAiOptions | undefined,
|
||||
contextualSearchFilters: FacetFilters | undefined
|
||||
): AskAiOptions | undefined {
|
||||
if (!askAi || !contextualSearchFilters || !askAi.indices?.length) {
|
||||
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 {
|
||||
...askAi,
|
||||
searchParameters: {
|
||||
...askAi.searchParameters,
|
||||
facetFilters: mergeFacetFilters(askAiFacetFilters, contextualSearchFilters),
|
||||
},
|
||||
searchParameters,
|
||||
};
|
||||
}
|
||||
|
||||
export function useAlgoliaAskAi(props: DocSearchPropsLite): UseAskAiResult {
|
||||
const [isAskAiActive, setIsAskAiActive] = useState(false);
|
||||
const contextualSearchFilters = useAlgoliaContextualFacetFiltersIfEnabled();
|
||||
|
||||
const askAi = useMemo(() => {
|
||||
return applyAskAiContextualSearch(props.askAi, contextualSearchFilters);
|
||||
}, [props.askAi, contextualSearchFilters]);
|
||||
|
||||
const askAiWithoutSidePanel = useMemo<AskAiConfigWithoutSidePanel | undefined>(() => {
|
||||
const resolvedAskAi = useMemo<UseAskAiResult['sidePanelAskAi']>(() => {
|
||||
if (!askAi) {
|
||||
return undefined;
|
||||
}
|
||||
const { sidePanel: _sidePanel, ...docsearchAskAi } = askAi;
|
||||
return docsearchAskAi;
|
||||
}, [askAi]);
|
||||
|
||||
const modalAskAi = useMemo<DocSearchAskAi | undefined>(() => {
|
||||
if (!askAiWithoutSidePanel) {
|
||||
return undefined;
|
||||
}
|
||||
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 {
|
||||
...askAi,
|
||||
apiKey: props.apiKey,
|
||||
appId: props.appId,
|
||||
indexName: getAskAiIndexName(askAi, props.indices),
|
||||
};
|
||||
}, [askAi, props.apiKey, props.appId, props.indices]);
|
||||
|
||||
return {
|
||||
canHandleAskAi,
|
||||
isAskAiActive,
|
||||
currentPlaceholder,
|
||||
onAskAiToggle,
|
||||
askAi,
|
||||
extraAskAiProps,
|
||||
modalAskAi: resolvedAskAi,
|
||||
sidePanelAskAi: resolvedAskAi,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,118 +1,42 @@
|
|||
/**
|
||||
* 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.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import type { AskAiConfig } from '@docsearch/docusaurus-adapter';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import type { ThemeConfigDocSearch } from '@docsearch/docusaurus-adapter';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
type AskAiTogglePayload = {
|
||||
query: string;
|
||||
messageId?: string;
|
||||
suggestedQuestionId?: string;
|
||||
};
|
||||
|
||||
type SidepanelOptions = Exclude<NonNullable<AskAiConfig['sidePanel']>, boolean>;
|
||||
type SidepanelOptions = Exclude<
|
||||
NonNullable<ThemeConfigDocSearch['sidePanel']>,
|
||||
boolean
|
||||
>;
|
||||
|
||||
type UseAlgoliaAskAiSidepanelParams = {
|
||||
askAiConfig?: AskAiConfig;
|
||||
importSidepanel: () => Promise<void>;
|
||||
sidePanel?: ThemeConfigDocSearch['sidePanel'];
|
||||
};
|
||||
|
||||
type UseAlgoliaAskAiSidepanelResult = {
|
||||
sidePanelEnabled: boolean;
|
||||
showSidepanelButton: boolean;
|
||||
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({
|
||||
askAiConfig,
|
||||
importSidepanel,
|
||||
sidePanel,
|
||||
}: UseAlgoliaAskAiSidepanelParams): UseAlgoliaAskAiSidepanelResult {
|
||||
const [isSidepanelOpen, setIsSidepanelOpen] = useState(false);
|
||||
const [sidepanelInitialMessage, setSidepanelInitialMessage] = useState<AskAiTogglePayload | undefined>(undefined);
|
||||
const openRequestId = useRef(0);
|
||||
|
||||
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 sidePanelOptions = useMemo(
|
||||
() => (typeof sidePanel === 'object' ? sidePanel : undefined),
|
||||
[sidePanel]
|
||||
);
|
||||
|
||||
const closeSidepanel = useCallback(() => {
|
||||
openRequestId.current += 1;
|
||||
setIsSidepanelOpen(false);
|
||||
setSidepanelInitialMessage(undefined);
|
||||
}, []);
|
||||
|
||||
const toggleSidepanel = useCallback(() => {
|
||||
if (isSidepanelOpen) {
|
||||
closeSidepanel();
|
||||
return;
|
||||
}
|
||||
openSidepanel();
|
||||
}, [closeSidepanel, isSidepanelOpen, openSidepanel]);
|
||||
|
||||
const handleSidepanelOpen = useCallback(() => {
|
||||
setIsSidepanelOpen(true);
|
||||
}, []);
|
||||
const sidePanelEnabled = Boolean(sidePanel);
|
||||
const showSidepanelButton =
|
||||
sidePanelEnabled && sidePanelOptions?.hideButton !== true;
|
||||
|
||||
return {
|
||||
sidePanelEnabled,
|
||||
showSidepanelButton,
|
||||
sidePanelOptions,
|
||||
sidePanelAgentStudio,
|
||||
sidepanelPortalContainer,
|
||||
isSidepanelOpen,
|
||||
sidepanelInitialMessage,
|
||||
openSidepanel,
|
||||
closeSidepanel,
|
||||
toggleSidepanel,
|
||||
handleSidepanelOpen,
|
||||
loadSidepanel,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
* 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.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { useDocsContextualSearchTags } from '@docusaurus/plugin-content-docs/client';
|
||||
|
|
@ -45,7 +45,9 @@ export function useAlgoliaContextualFacetFilters(): FacetFilters {
|
|||
return [languageFilter, tagsFilter];
|
||||
}
|
||||
|
||||
export function useAlgoliaContextualFacetFiltersIfEnabled(): FacetFilters | undefined {
|
||||
export function useAlgoliaContextualFacetFiltersIfEnabled():
|
||||
| FacetFilters
|
||||
| undefined {
|
||||
const { contextualSearch } = useAlgoliaThemeConfig();
|
||||
const facetFilters = useAlgoliaContextualFacetFilters();
|
||||
if (contextualSearch) {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
import type {
|
||||
ThemeConfig,
|
||||
ThemeConfigDocSearch,
|
||||
} from '@docsearch/docusaurus-adapter';
|
||||
/**
|
||||
* 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.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
import type { ThemeConfig, ThemeConfigAlgolia } from '@docsearch/docusaurus-adapter';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
|
||||
import { getDocSearchConfig } from '../getDocSearchConfig';
|
||||
|
||||
export function useAlgoliaThemeConfig(): ThemeConfigAlgolia {
|
||||
export function useAlgoliaThemeConfig(): ThemeConfigDocSearch {
|
||||
const {
|
||||
siteConfig: { themeConfig },
|
||||
} = useDocusaurusContext();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
/**
|
||||
* 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.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* 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 { useBaseUrlUtils } from '@docusaurus/useBaseUrl';
|
||||
import { useCallback } from 'react';
|
||||
|
|
@ -14,10 +14,13 @@ import { useAlgoliaThemeConfig } from './useAlgoliaThemeConfig';
|
|||
|
||||
function replacePathname(
|
||||
pathname: string,
|
||||
replaceSearchResultPathname: ThemeConfigAlgolia['replaceSearchResultPathname'],
|
||||
replaceSearchResultPathname: ThemeConfigDocSearch['replaceSearchResultPathname']
|
||||
): string {
|
||||
return replaceSearchResultPathname
|
||||
? pathname.replaceAll(new RegExp(replaceSearchResultPathname.from, 'g'), replaceSearchResultPathname.to)
|
||||
? pathname.replaceAll(
|
||||
new RegExp(replaceSearchResultPathname.from, 'g'),
|
||||
replaceSearchResultPathname.to
|
||||
)
|
||||
: pathname;
|
||||
}
|
||||
|
||||
|
|
@ -27,7 +30,8 @@ function replacePathname(
|
|||
*/
|
||||
export function useSearchResultUrlProcessor(): (url: string) => string {
|
||||
const { withBaseUrl } = useBaseUrlUtils();
|
||||
const { externalUrlRegex, replaceSearchResultPathname } = useAlgoliaThemeConfig();
|
||||
const { externalUrlRegex, replaceSearchResultPathname } =
|
||||
useAlgoliaThemeConfig();
|
||||
|
||||
return useCallback(
|
||||
(url: string) => {
|
||||
|
|
@ -41,8 +45,10 @@ export function useSearchResultUrlProcessor(): (url: string) => string {
|
|||
// Otherwise => transform to relative URL for SPA navigation
|
||||
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.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import type { FacetFilters } from 'algoliasearch/lite';
|
||||
|
||||
export function mergeFacetFilters(f1: FacetFilters, f2: FacetFilters): FacetFilters;
|
||||
|
||||
export function mergeFacetFilters(f1: FacetFilters | undefined, f2: FacetFilters | undefined): FacetFilters | undefined;
|
||||
export function mergeFacetFilters(
|
||||
f1: FacetFilters,
|
||||
f2: FacetFilters
|
||||
): FacetFilters;
|
||||
|
||||
export function mergeFacetFilters(
|
||||
f1: FacetFilters | undefined,
|
||||
f2: FacetFilters | undefined,
|
||||
f2: FacetFilters | undefined
|
||||
): FacetFilters | undefined;
|
||||
|
||||
export function mergeFacetFilters(
|
||||
f1: FacetFilters | undefined,
|
||||
f2: FacetFilters | undefined
|
||||
): FacetFilters | undefined {
|
||||
if (f1 === undefined) {
|
||||
return f2;
|
||||
|
|
@ -22,6 +28,7 @@ export function mergeFacetFilters(
|
|||
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)];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
/**
|
||||
* 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.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
declare module '@docsearch/react/modal';
|
||||
declare module '@docsearch/react/style';
|
||||
declare module '@docsearch/react/style/sidepanel';
|
||||
|
||||
|
|
@ -14,6 +13,6 @@ declare module 'eta' {
|
|||
|
||||
export function compile(
|
||||
template: string,
|
||||
options?: Record<string, unknown>,
|
||||
options?: Record<string, unknown>
|
||||
): (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.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* 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 {
|
||||
return Boolean(themeConfig.algolia);
|
||||
}
|
||||
|
||||
export function getDocSearchConfig(themeConfig: ThemeConfig): ThemeConfigAlgolia {
|
||||
export function getDocSearchConfig(
|
||||
themeConfig: ThemeConfig
|
||||
): ThemeConfigDocSearch {
|
||||
if (themeConfig.docsearch) {
|
||||
return themeConfig.docsearch;
|
||||
}
|
||||
|
||||
if (themeConfig.algolia) {
|
||||
return themeConfig.algolia;
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import type { ThemeConfig } from '@docsearch/docusaurus-adapter';
|
||||
import { readDefaultCodeTranslationMessages } from '@docusaurus/theme-translations';
|
||||
import type { LoadContext, Plugin } from '@docusaurus/types';
|
||||
|
||||
import { getDocSearchConfig, hasLegacyAlgoliaConfig } from './getDocSearchConfig';
|
||||
import { createOpenSearchFile, createOpenSearchHeadTags, shouldCreateOpenSearchFile } from './opensearch';
|
||||
import { getDocSearchConfig } from './getDocSearchConfig';
|
||||
import {
|
||||
createOpenSearchFile,
|
||||
createOpenSearchHeadTags,
|
||||
shouldCreateOpenSearchFile,
|
||||
} from './opensearch';
|
||||
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> {
|
||||
const {
|
||||
baseUrl,
|
||||
siteConfig: { themeConfig },
|
||||
i18n: { currentLocale },
|
||||
} = context;
|
||||
const { searchPagePath } = getDocSearchConfig(themeConfig as ThemeConfig);
|
||||
const classicPresetWithLegacyAlgoliaConfig =
|
||||
hasClassicPreset(context) && hasLegacyAlgoliaConfig(themeConfig as ThemeConfig);
|
||||
const { searchPage } = getDocSearchConfig(themeConfig as ThemeConfig);
|
||||
const searchPagePath = searchPage === false ? false : searchPage.path;
|
||||
|
||||
return {
|
||||
name: 'docsearch-docusaurus-algolia-search',
|
||||
|
|
@ -55,9 +44,7 @@ export default function themeSearchAlgolia(context: LoadContext): Plugin<void> {
|
|||
},
|
||||
|
||||
contentLoaded({ actions: { addRoute } }) {
|
||||
// The classic preset adds /search through @docusaurus/theme-search-algolia,
|
||||
// but only when the legacy "themeConfig.algolia" key is used.
|
||||
if (searchPagePath && !classicPresetWithLegacyAlgoliaConfig) {
|
||||
if (searchPagePath) {
|
||||
addRoute({
|
||||
path: normalizeUrl([baseUrl, searchPagePath]),
|
||||
component: '@theme/SearchPage',
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
* 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.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
|
@ -11,13 +11,20 @@ import type { ThemeConfig } from '@docsearch/docusaurus-adapter';
|
|||
import type { HtmlTags, LoadContext } from '@docusaurus/types';
|
||||
import { defaultConfig, compile } from 'eta';
|
||||
import fs from 'fs-extra';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { getDocSearchConfig } from './getDocSearchConfig';
|
||||
import openSearchTemplate from './templates/opensearch';
|
||||
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: {
|
||||
title: string;
|
||||
|
|
@ -31,14 +38,19 @@ function renderOpenSearchTemplate(data: {
|
|||
|
||||
const OPEN_SEARCH_FILENAME = 'opensearch.xml';
|
||||
|
||||
export function shouldCreateOpenSearchFile({ context }: { context: LoadContext }): boolean {
|
||||
export function shouldCreateOpenSearchFile({
|
||||
context,
|
||||
}: {
|
||||
context: LoadContext;
|
||||
}): boolean {
|
||||
const {
|
||||
siteConfig: {
|
||||
themeConfig,
|
||||
future: { experimental_router: router },
|
||||
},
|
||||
} = 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';
|
||||
}
|
||||
|
|
@ -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 {
|
||||
outDir,
|
||||
siteConfig: { themeConfig },
|
||||
} = context;
|
||||
const { searchPagePath } = getDocSearchConfig(themeConfig as ThemeConfig);
|
||||
const { searchPage } = getDocSearchConfig(themeConfig as ThemeConfig);
|
||||
const searchPagePath = searchPage === false ? false : searchPage.path;
|
||||
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 });
|
||||
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 {
|
||||
baseUrl,
|
||||
siteConfig: { title },
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
* 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.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
export default `
|
||||
|
|
|
|||
|
|
@ -1,97 +1,120 @@
|
|||
/**
|
||||
* 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.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
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 { FacetFilters } from 'algoliasearch/lite';
|
||||
import type { DeepPartial, Overwrite, Optional } from 'utility-types';
|
||||
|
||||
type AskAiSearchParameters = {
|
||||
facetFilters?: FacetFilters;
|
||||
filters?: string;
|
||||
attributesToRetrieve?: string[];
|
||||
restrictSearchableAttributes?: string[];
|
||||
distinct?: boolean | number | string;
|
||||
type DocusaurusSidePanelConfig =
|
||||
| boolean
|
||||
| (Omit<SidepanelProps, 'tools'> & { hideButton?: boolean });
|
||||
type DocusaurusSearchBarSidePanelProps =
|
||||
| boolean
|
||||
| (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 = {
|
||||
indexName: string;
|
||||
apiKey: string;
|
||||
appId: string;
|
||||
assistantId: string;
|
||||
suggestedQuestions?: boolean;
|
||||
useStagingEnv?: boolean;
|
||||
sidePanel?: boolean | (SidepanelProps & { hideButton?: boolean });
|
||||
} & (
|
||||
| {
|
||||
agentStudio: false;
|
||||
searchParameters?: AskAiSearchParameters;
|
||||
}
|
||||
| {
|
||||
agentStudio: true;
|
||||
searchParameters?: AgentStudioSearchParameters;
|
||||
}
|
||||
| {
|
||||
agentStudio?: never;
|
||||
searchParameters?: AskAiSearchParameters;
|
||||
}
|
||||
);
|
||||
agentId: DocSearchAskAi['agentId'];
|
||||
suggestedQuestions?: DocSearchAskAi['suggestedQuestions'];
|
||||
searchParameters?: AgentStudioSearchParameters;
|
||||
indices?: string[];
|
||||
memory?: DocSearchAskAi['memory'];
|
||||
promptSuggestions?: DocSearchAskAi['promptSuggestions'];
|
||||
};
|
||||
|
||||
export type DocusaurusSearchBarAskAiProps = AskAiConfig &
|
||||
Pick<DocSearchAskAi, 'tools'>;
|
||||
|
||||
// DocSearch props that Docusaurus exposes directly through props forwarding
|
||||
type DocusaurusDocSearchProps = Pick<
|
||||
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;
|
||||
sidePanel?: DocusaurusSidePanelConfig;
|
||||
};
|
||||
|
||||
export type ThemeConfigAlgolia = DocusaurusDocSearchProps & {
|
||||
indexName: string;
|
||||
|
||||
export type ThemeConfigDocSearch = DocusaurusDocSearchProps & {
|
||||
// Docusaurus custom options, not coming from DocSearch
|
||||
contextualSearch: boolean;
|
||||
externalUrlRegex?: string;
|
||||
searchPagePath: string | false | null;
|
||||
searchPage: SearchPageConfig;
|
||||
replaceSearchResultPathname?: {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
};
|
||||
|
||||
type UserDocSearchConfig = Overwrite<
|
||||
DeepPartial<ThemeConfigAlgolia>,
|
||||
{
|
||||
// Required fields:
|
||||
appId: ThemeConfigAlgolia['appId'];
|
||||
apiKey: ThemeConfigAlgolia['apiKey'];
|
||||
indexName: ThemeConfigAlgolia['indexName'];
|
||||
// askAi also accepts a shorter string form
|
||||
askAi?: Optional<AskAiConfig, 'apiKey' | 'appId' | 'indexName'> | string;
|
||||
export type DocusaurusSearchBarProps = Partial<
|
||||
Omit<ThemeConfigDocSearch, 'askAi' | 'sidePanel'> & {
|
||||
askAi?: DocusaurusSearchBarAskAiProps;
|
||||
sidePanel?: DocusaurusSearchBarSidePanelProps;
|
||||
}
|
||||
>;
|
||||
|
||||
type UserDocSearchConfig = Omit<
|
||||
Partial<ThemeConfigDocSearch>,
|
||||
'apiKey' | 'appId' | 'askAi' | 'indices'
|
||||
> & {
|
||||
appId: ThemeConfigDocSearch['appId'];
|
||||
apiKey: ThemeConfigDocSearch['apiKey'];
|
||||
indices: ThemeConfigDocSearch['indices'];
|
||||
askAi?: AskAiConfig;
|
||||
};
|
||||
|
||||
export type ThemeConfig = {
|
||||
// Preferred key.
|
||||
docsearch?: ThemeConfigAlgolia;
|
||||
// Backward-compatible alias.
|
||||
algolia?: ThemeConfigAlgolia;
|
||||
docsearch?: ThemeConfigDocSearch;
|
||||
};
|
||||
|
||||
export type UserThemeConfig = {
|
||||
// Preferred key.
|
||||
docsearch?: UserDocSearchConfig;
|
||||
// Backward-compatible alias.
|
||||
algolia?: UserDocSearchConfig;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -102,9 +125,12 @@ declare module '@theme/SearchPage' {
|
|||
}
|
||||
|
||||
declare module '@theme/SearchBar' {
|
||||
import type { DocusaurusSearchBarProps } from '@docsearch/docusaurus-adapter';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function SearchBar(): ReactNode;
|
||||
export default function SearchBar(
|
||||
props?: DocusaurusSearchBarProps
|
||||
): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/SearchTranslations' {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,34 @@
|
|||
/* 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.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import type { AutocompleteState } from '@algolia/autocomplete-core';
|
||||
import type { ThemeConfigAlgolia } from '@docsearch/docusaurus-adapter';
|
||||
import { DocSearch as DocSearchProvider, useDocSearch } from '@docsearch/core';
|
||||
import type {
|
||||
InternalDocSearchHit,
|
||||
DocSearchModal as DocSearchModalType,
|
||||
DocSearchModalProps,
|
||||
StoredDocSearchHit,
|
||||
DocSearchTransformClient,
|
||||
DocusaurusSearchBarAskAiProps,
|
||||
ThemeConfigDocSearch,
|
||||
} from '@docsearch/docusaurus-adapter';
|
||||
import type { DocSearchAskAiModal as DocSearchAskAiModalType } from '@docsearch/modal/askai';
|
||||
import { DocSearchButton } from '@docsearch/modal/button';
|
||||
import type { DocSearchModal as DocSearchModalType } from '@docsearch/modal/modal';
|
||||
import type {
|
||||
DocSearchAskAiModalProps,
|
||||
DocSearchHit,
|
||||
DocSearchModalProps,
|
||||
DocSearchProps,
|
||||
DocSearchTransformClient,
|
||||
DocSearchTranslations,
|
||||
InternalDocSearchHit,
|
||||
StoredDocSearchHit,
|
||||
ToolCalls,
|
||||
} from '@docsearch/react';
|
||||
import { DocSearchButton } from '@docsearch/react/button';
|
||||
import { SidepanelButton } from '@docsearch/react/sidepanel';
|
||||
import type { Sidepanel as SidepanelType } from '@docsearch/react/sidepanel';
|
||||
import { useDocSearchKeyboardEvents } from '@docsearch/react/useDocSearchKeyboardEvents';
|
||||
import { SidepanelButton } from '@docsearch/sidepanel/button';
|
||||
import type { Sidepanel as SidepanelType } from '@docsearch/sidepanel/sidepanel';
|
||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||
import Head from '@docusaurus/Head';
|
||||
import Link from '@docusaurus/Link';
|
||||
import { useHistory } from '@docusaurus/router';
|
||||
|
|
@ -28,74 +37,100 @@ import Translate from '@docusaurus/Translate';
|
|||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import translations from '@theme/SearchTranslations';
|
||||
import type { FacetFilters } from 'algoliasearch/lite';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type JSX,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
mergeFacetFilters,
|
||||
useAlgoliaAskAi,
|
||||
useAlgoliaAskAiSidepanel,
|
||||
useAlgoliaContextualFacetFilters,
|
||||
useAlgoliaThemeConfig,
|
||||
useSearchResultUrlProcessor,
|
||||
useAlgoliaAskAi,
|
||||
useAlgoliaAskAiSidepanel,
|
||||
mergeFacetFilters,
|
||||
} from '../../client';
|
||||
|
||||
type DocSearchProps = Omit<DocSearchModalProps, 'initialScrollY' | 'onClose'> & {
|
||||
contextualSearch?: string;
|
||||
type NavigatorNavigateParams = Parameters<
|
||||
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;
|
||||
searchPagePath: boolean | string;
|
||||
askAi?: Exclude<(DocSearchModalProps & { askAi: unknown })['askAi'], string | undefined>;
|
||||
};
|
||||
|
||||
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'];
|
||||
indices: NonNullable<DocSearchProps['indices']>;
|
||||
searchPage: ThemeConfigDocSearch['searchPage'];
|
||||
sidePanel?: SidePanelOptions | boolean;
|
||||
translations?: DocSearchTranslations;
|
||||
}
|
||||
};
|
||||
|
||||
let DocSearchModal: typeof DocSearchModalType | null = null;
|
||||
let DocSearchAskAiModal: typeof DocSearchAskAiModalType | null = null;
|
||||
let DocSearchSidepanel: typeof SidepanelType | null = null;
|
||||
|
||||
function importDocSearchModalIfNeeded(): Promise<void> {
|
||||
if (DocSearchModal) {
|
||||
if (DocSearchModal && DocSearchAskAiModal) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/dynamic-import-chunkname
|
||||
return Promise.all([import('@docsearch/react/modal'), import('@docsearch/react/style'), import('./styles.css')]).then(
|
||||
([{ DocSearchModal: Modal }]) => {
|
||||
return Promise.all([
|
||||
import('@docsearch/modal/modal'),
|
||||
import('@docsearch/modal/askai'),
|
||||
import('@docsearch/react/style'),
|
||||
import('./styles.css'),
|
||||
]).then(
|
||||
([{ DocSearchModal: Modal }, { DocSearchAskAiModal: AskAiModal }]) => {
|
||||
DocSearchModal = Modal;
|
||||
},
|
||||
DocSearchAskAiModal = AskAiModal;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function importDocSearchSidepanelIfNeeded(): Promise<void> {
|
||||
await importDocSearchModalIfNeeded();
|
||||
function importDocSearchSidepanelIfNeeded(): Promise<void> {
|
||||
if (DocSearchSidepanel) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/dynamic-import-chunkname
|
||||
return Promise.all([import('@docsearch/react/sidepanel'), import('@docsearch/react/style/sidepanel')]).then(
|
||||
([{ Sidepanel }]) => {
|
||||
DocSearchSidepanel = Sidepanel;
|
||||
},
|
||||
);
|
||||
return Promise.all([
|
||||
import('@docsearch/sidepanel/sidepanel'),
|
||||
import('@docsearch/react/style/sidepanel'),
|
||||
]).then(([{ Sidepanel }]) => {
|
||||
DocSearchSidepanel = Sidepanel;
|
||||
});
|
||||
}
|
||||
|
||||
function useNavigator({
|
||||
externalUrlRegex,
|
||||
}: Pick<DocSearchProps, 'externalUrlRegex'>): DocSearchModalProps['navigator'] {
|
||||
}: Pick<
|
||||
AdapterDocSearchProps,
|
||||
'externalUrlRegex'
|
||||
>): DocSearchModalProps['navigator'] {
|
||||
const history = useHistory();
|
||||
const [navigator] = useState<DocSearchModalProps['navigator']>(() => {
|
||||
return {
|
||||
|
|
@ -122,23 +157,27 @@ function useTransformSearchClient(): DocSearchModalProps['transformSearchClient'
|
|||
searchClient.addAlgoliaAgent('docusaurus', docusaurusVersion);
|
||||
return searchClient;
|
||||
},
|
||||
[docusaurusVersion],
|
||||
[docusaurusVersion]
|
||||
);
|
||||
}
|
||||
|
||||
function useTransformItems(props: Pick<DocSearchProps, 'transformItems'>) {
|
||||
function useTransformItems(
|
||||
props: Pick<AdapterDocSearchProps, 'transformItems'>
|
||||
): DocSearchModalProps['transformItems'] {
|
||||
const processSearchResultUrl = useSearchResultUrlProcessor();
|
||||
const [transformItems] = useState<DocSearchModalProps['transformItems']>(() => {
|
||||
return (items: DocSearchHit[]) =>
|
||||
props.transformItems
|
||||
? // Custom transformItems
|
||||
props.transformItems(items)
|
||||
: // Default transformItems
|
||||
items.map((item) => ({
|
||||
...item,
|
||||
url: processSearchResultUrl(item.url),
|
||||
}));
|
||||
});
|
||||
const [transformItems] = useState<DocSearchModalProps['transformItems']>(
|
||||
() => {
|
||||
return (items: DocSearchHit[]) =>
|
||||
props.transformItems
|
||||
? // Custom transformItems
|
||||
props.transformItems(items)
|
||||
: // Default transformItems
|
||||
items.map((item) => ({
|
||||
...item,
|
||||
url: processSearchResultUrl(item.url),
|
||||
}));
|
||||
}
|
||||
);
|
||||
return transformItems;
|
||||
}
|
||||
|
||||
|
|
@ -148,17 +187,29 @@ function useResultsFooterComponent({
|
|||
}: {
|
||||
closeModal: () => void;
|
||||
searchPagePath?: string;
|
||||
}): DocSearchProps['resultsFooterComponent'] {
|
||||
}): DocSearchModalProps['resultsFooterComponent'] {
|
||||
return useMemo(
|
||||
() =>
|
||||
searchPagePath
|
||||
? ({ state }) => <ResultsFooter state={state} searchPagePath={searchPagePath} onClose={closeModal} />
|
||||
? ({ state }) => (
|
||||
<ResultsFooter
|
||||
state={state}
|
||||
searchPagePath={searchPagePath}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
)
|
||||
: 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>;
|
||||
}
|
||||
|
||||
|
|
@ -168,7 +219,11 @@ type ResultsFooterProps = {
|
|||
searchPagePath: string;
|
||||
};
|
||||
|
||||
function ResultsFooter({ state, onClose, searchPagePath }: ResultsFooterProps) {
|
||||
function ResultsFooter({
|
||||
state,
|
||||
onClose,
|
||||
searchPagePath,
|
||||
}: ResultsFooterProps): JSX.Element {
|
||||
const searchPageLink = useBaseUrl(searchPagePath);
|
||||
const nbHits = (state.context as { nbHits?: number }).nbHits ?? 0;
|
||||
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 configFacetFilters: FacetFilters = props.searchParameters?.facetFilters ?? [];
|
||||
return useMemo(() => {
|
||||
if (!contextualSearch) {
|
||||
return indices;
|
||||
}
|
||||
|
||||
const facetFilters: FacetFilters = contextualSearch
|
||||
? // Merge contextual search filters with config filters
|
||||
mergeFacetFilters(contextualSearchFacetFilters, configFacetFilters)
|
||||
: // ... or use config facetFilters
|
||||
configFacetFilters;
|
||||
return indices.map((index) => {
|
||||
if (typeof index === 'string') {
|
||||
return {
|
||||
name: index,
|
||||
searchParameters: {
|
||||
facetFilters: contextualSearchFacetFilters,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// We let users override default searchParameters if they want to
|
||||
return {
|
||||
...props.searchParameters,
|
||||
facetFilters,
|
||||
};
|
||||
return {
|
||||
...index,
|
||||
searchParameters: {
|
||||
...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 searchParameters = useSearchParameters({ ...props } as DocSearchProps);
|
||||
const indices = useSearchIndices({
|
||||
contextualSearch,
|
||||
indices: props.indices,
|
||||
});
|
||||
const transformItems = useTransformItems(props);
|
||||
const transformSearchClient = useTransformSearchClient();
|
||||
|
||||
const searchContainer = useRef<HTMLDivElement | null>(null);
|
||||
const searchButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [initialQuery, setInitialQuery] = useState<string | undefined>(undefined);
|
||||
|
||||
const { isAskAiActive, currentPlaceholder, onAskAiToggle, extraAskAiProps, askAi } = useAlgoliaAskAi(props);
|
||||
const {
|
||||
sidePanelEnabled,
|
||||
showSidepanelButton,
|
||||
sidePanelOptions,
|
||||
sidePanelAgentStudio,
|
||||
sidepanelPortalContainer,
|
||||
isSidepanelOpen,
|
||||
sidepanelInitialMessage,
|
||||
openSidepanel,
|
||||
closeSidepanel,
|
||||
toggleSidepanel,
|
||||
handleSidepanelOpen,
|
||||
loadSidepanel,
|
||||
} = useAlgoliaAskAiSidepanel({
|
||||
askAiConfig: askAi,
|
||||
importSidepanel: importDocSearchSidepanelIfNeeded,
|
||||
const { closeModal, isModalActive } = useDocSearch();
|
||||
const [modalLoaded, setModalLoaded] = useState(
|
||||
Boolean(DocSearchModal && DocSearchAskAiModal)
|
||||
);
|
||||
const [sidepanelLoaded, setSidepanelLoaded] = useState(
|
||||
Boolean(DocSearchSidepanel)
|
||||
);
|
||||
const { modalAskAi, sidePanelAskAi } = useAlgoliaAskAi({
|
||||
appId: props.appId,
|
||||
apiKey: props.apiKey,
|
||||
indices: props.indices,
|
||||
askAi,
|
||||
});
|
||||
|
||||
const prepareSearchContainer = useCallback(() => {
|
||||
if (!searchContainer.current) {
|
||||
const divElement = document.createElement('div');
|
||||
searchContainer.current = divElement;
|
||||
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 { sidePanelEnabled, showSidepanelButton, sidePanelOptions } =
|
||||
useAlgoliaAskAiSidepanel({
|
||||
sidePanel,
|
||||
});
|
||||
const searchPagePath = getSearchPagePath(searchPage);
|
||||
const resultsFooterComponent = useResultsFooterComponent({
|
||||
closeModal,
|
||||
searchPagePath: resultsFooterSearchPagePath,
|
||||
searchPagePath,
|
||||
});
|
||||
|
||||
useDocSearchKeyboardEvents({
|
||||
isOpen,
|
||||
onOpen: openModal,
|
||||
onClose: closeModal,
|
||||
onInput: handleInput,
|
||||
searchButtonRef,
|
||||
isAskAiActive: isAskAiActive ?? false,
|
||||
onAskAiToggle: onAskAiToggle ?? (() => {}),
|
||||
});
|
||||
const loadModal = useCallback(() => {
|
||||
return importDocSearchModalIfNeeded().then(() => setModalLoaded(true));
|
||||
}, []);
|
||||
|
||||
const loadSidepanel = useCallback(() => {
|
||||
return importDocSearchSidepanelIfNeeded().then(() =>
|
||||
setSidepanelLoaded(true)
|
||||
);
|
||||
}, []);
|
||||
|
||||
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 (
|
||||
<>
|
||||
|
|
@ -310,103 +380,87 @@ function DocSearch({ externalUrlRegex, ...props }: AlgoliaSearchBarProps) {
|
|||
{/* 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
|
||||
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>
|
||||
|
||||
<div className="DocSearch-SearchBar">
|
||||
<DocSearchButton
|
||||
ref={searchButtonRef}
|
||||
translations={props.translations?.button ?? translations.button}
|
||||
onTouchStart={importDocSearchModalIfNeeded}
|
||||
onFocus={importDocSearchModalIfNeeded}
|
||||
onMouseOver={importDocSearchModalIfNeeded}
|
||||
onClick={openModal}
|
||||
onTouchStart={loadModal}
|
||||
onFocus={loadModal}
|
||||
onMouseOver={loadModal}
|
||||
/>
|
||||
{showSidepanelButton && (
|
||||
<SidepanelButton
|
||||
translations={{
|
||||
buttonText: '',
|
||||
buttonAriaLabel: 'Ask AI',
|
||||
}}
|
||||
variant={sidePanelOptions?.variant ?? 'inline'}
|
||||
keyboardShortcuts={sidePanelOptions?.keyboardShortcuts}
|
||||
onTouchStart={loadSidepanel}
|
||||
onFocus={loadSidepanel}
|
||||
onMouseOver={loadSidepanel}
|
||||
onClick={toggleSidepanel}
|
||||
/>
|
||||
)}
|
||||
{ExecutionEnvironment.canUseDOM &&
|
||||
showSidepanelButton &&
|
||||
sidePanelAskAi && (
|
||||
<SidepanelButton
|
||||
translations={{
|
||||
buttonText: '',
|
||||
buttonAriaLabel: 'Ask AI',
|
||||
}}
|
||||
variant={sidePanelOptions?.variant ?? 'inline'}
|
||||
keyboardShortcuts={sidePanelOptions?.keyboardShortcuts}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isOpen &&
|
||||
DocSearchModal &&
|
||||
searchContainer.current &&
|
||||
createPortal(
|
||||
<DocSearchModal
|
||||
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,
|
||||
)}
|
||||
{modalLoaded &&
|
||||
(modalAskAi && DocSearchAskAiModal ? (
|
||||
<DocSearchAskAiModal {...modalProps} askAi={modalAskAi} />
|
||||
) : (
|
||||
DocSearchModal && <DocSearchModal {...modalProps} />
|
||||
))}
|
||||
|
||||
{sidePanelEnabled &&
|
||||
DocSearchSidepanel &&
|
||||
askAi &&
|
||||
sidepanelPortalContainer &&
|
||||
createPortal(
|
||||
{ExecutionEnvironment.canUseDOM &&
|
||||
sidePanelEnabled &&
|
||||
sidepanelLoaded &&
|
||||
sidePanelAskAi &&
|
||||
DocSearchSidepanel && (
|
||||
<DocSearchSidepanel
|
||||
{...sidePanelOptions}
|
||||
variant={sidePanelOptions?.variant ?? 'inline'}
|
||||
pushSelector={sidePanelOptions?.pushSelector ?? '#__docusaurus'}
|
||||
assistantId={askAi.assistantId}
|
||||
apiKey={askAi.apiKey}
|
||||
appId={askAi.appId}
|
||||
indexName={askAi.indexName}
|
||||
agentStudio={sidePanelAgentStudio}
|
||||
suggestedQuestions={sidePanelOptions?.suggestedQuestions ?? askAi.suggestedQuestions}
|
||||
isOpen={isSidepanelOpen}
|
||||
initialMessage={sidepanelInitialMessage}
|
||||
onOpen={handleSidepanelOpen}
|
||||
onClose={closeSidepanel}
|
||||
/>,
|
||||
sidepanelPortalContainer,
|
||||
{...panelOptions}
|
||||
variant={panelOptions.variant ?? 'inline'}
|
||||
pushSelector={panelOptions.pushSelector ?? '#__docusaurus'}
|
||||
agentId={sidePanelAskAi.agentId}
|
||||
apiKey={sidePanelAskAi.apiKey}
|
||||
appId={sidePanelAskAi.appId}
|
||||
searchParameters={sidePanelAskAi.searchParameters}
|
||||
indices={panelOptions.indices ?? sidePanelAskAi.indices}
|
||||
suggestedQuestions={
|
||||
panelOptions.suggestedQuestions ??
|
||||
sidePanelAskAi.suggestedQuestions
|
||||
}
|
||||
tools={panelOptions.tools ?? sidePanelAskAi.tools}
|
||||
memory={panelOptions.memory ?? sidePanelAskAi.memory}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SearchBar(props: Partial<AlgoliaSearchBarProps>): ReactNode {
|
||||
export default function SearchBar(
|
||||
props: Partial<AdapterDocSearchProps>
|
||||
): ReactNode {
|
||||
const themeConfig = useAlgoliaThemeConfig();
|
||||
|
||||
const docSearchProps: AlgoliaSearchBarProps = {
|
||||
...(themeConfig as unknown as AlgoliaSearchBarProps),
|
||||
const docSearchProps: AdapterDocSearchProps = {
|
||||
...(themeConfig as AdapterDocSearchProps),
|
||||
// Let props override theme config
|
||||
// See https://github.com/facebook/docusaurus/pull/11581
|
||||
...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 {
|
||||
--docsearch-font-family: var(--ifm-font-family-base);
|
||||
--docsearch-primary-color: var(--ifm-color-primary);
|
||||
--docsearch-text-color: var(--ifm-font-color-base);
|
||||
--docsearch-border-radius: var(--ifm-global-radius);
|
||||
--docsearch-highlight-color: var(--ifm-color-primary);
|
||||
--docsearch-soft-primary-color: color-mix(
|
||||
in srgb,
|
||||
var(--ifm-color-primary) 12%,
|
||||
transparent
|
||||
);
|
||||
--docsearch-soft-primary-color: color-mix(in srgb,
|
||||
var(--ifm-color-primary) 12%,
|
||||
transparent);
|
||||
--docsearch-focus-color: var(--ifm-color-primary-dark);
|
||||
--docsearch-subtle-color: var(--ifm-color-emphasis-200);
|
||||
--docsearch-secondary-text-color: var(--ifm-color-emphasis-600);
|
||||
--docsearch-icon-color: var(--ifm-color-emphasis-600);
|
||||
--docsearch-muted-color: var(--ifm-color-emphasis-600);
|
||||
--docsearch-secondary-text-color: var(--ifm-color-secondary-darker);
|
||||
--docsearch-icon-color: var(--ifm-color-secondary-darker);
|
||||
--docsearch-muted-color: var(--ifm-color-secondary-darker);
|
||||
--docsearch-container-background: rgb(0 0 0 / 60%);
|
||||
--docsearch-background-color: var(--ifm-color-emphasis-200);
|
||||
--docsearch-background-color: var(--ifm-color-secondary);
|
||||
|
||||
/* Modal */
|
||||
--docsearch-modal-background: var(--ifm-color-emphasis-100);
|
||||
--docsearch-modal-background: var(--ifm-background-color);
|
||||
--docsearch-modal-shadow: var(--ifm-global-shadow-md);
|
||||
|
||||
/* Button */
|
||||
--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 */
|
||||
--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 */
|
||||
--docsearch-hit-color: var(--ifm-font-color-base);
|
||||
--docsearch-hit-highlight-color: color-mix(
|
||||
in srgb,
|
||||
var(--ifm-color-primary) 12%,
|
||||
transparent
|
||||
);
|
||||
--docsearch-hit-highlight-color: color-mix(in srgb,
|
||||
var(--ifm-color-primary) 12%,
|
||||
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-background: var(--ifm-background-surface-color);
|
||||
--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%);
|
||||
|
||||
/* 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-key-gradient: linear-gradient(
|
||||
-26.5deg,
|
||||
var(--ifm-color-emphasis-300) 0%,
|
||||
var(--ifm-color-emphasis-200) 100%
|
||||
);
|
||||
--docsearch-key-gradient: linear-gradient(-26.5deg,
|
||||
var(--ifm-color-emphasis-300) 0%,
|
||||
var(--ifm-color-emphasis-200) 100%);
|
||||
|
||||
/* 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-white: var(--ifm-background-color);
|
||||
--docsearch-sidepanel-primary: var(--docsearch-primary-color);
|
||||
|
|
@ -68,15 +69,13 @@
|
|||
--docsearch-sidepanel-scrollbar-bg: var(--docsearch-modal-background);
|
||||
--docsearch-sidepanel-hit-background: var(--docsearch-hit-background);
|
||||
--docsearch-sidepanel-hit-color: var(--docsearch-hit-color);
|
||||
--docsearch-sidepanel-hit-highlight-color: var(
|
||||
--docsearch-hit-highlight-color
|
||||
);
|
||||
--docsearch-sidepanel-hit-highlight-color: var(--docsearch-hit-highlight-color);
|
||||
}
|
||||
|
||||
.DocSearch-SearchBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.DocSearch-Button {
|
||||
|
|
@ -95,6 +94,10 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.DocSearch-Sidepanel .DocSearch-AskAiScreen-Query {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.DocSearch-SidepanelButton.inline {
|
||||
background-color: var(--ifm-background-color);
|
||||
}
|
||||
|
|
@ -105,15 +108,20 @@
|
|||
}
|
||||
|
||||
/* 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 {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.DocSearch-Sidepanel-Container.inline.side-left {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-AskAiScreen-Query {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.DocSearch-Container {
|
||||
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.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* 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 Link from '@docusaurus/Link';
|
||||
import { useAllDocsData } from '@docusaurus/plugin-content-docs/client';
|
||||
import { HtmlClassNameProvider, useEvent, usePluralForm, useSearchQueryString } from '@docusaurus/theme-common';
|
||||
import { HtmlClassNameProvider, usePluralForm } from '@docusaurus/theme-common';
|
||||
import Translate, { translate } from '@docusaurus/Translate';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import Heading from '@theme/Heading';
|
||||
import Layout from '@theme/Layout';
|
||||
import { liteClient } from 'algoliasearch/lite';
|
||||
import algoliaSearchHelper from 'algoliasearch-helper';
|
||||
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 type { SearchResultItem } from './types';
|
||||
|
||||
// Very simple pluralization: probably good enough for now
|
||||
function useDocumentsFoundPlural() {
|
||||
function useDocumentsFoundPlural(): (count: number) => string {
|
||||
const { selectMessage } = usePluralForm();
|
||||
return (count: number) =>
|
||||
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)',
|
||||
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 {
|
||||
return searchQuery
|
||||
? translate(
|
||||
|
|
@ -188,7 +57,7 @@ function getSearchPageTitle(searchQuery: string | undefined): string {
|
|||
},
|
||||
{
|
||||
query: searchQuery,
|
||||
},
|
||||
}
|
||||
)
|
||||
: translate({
|
||||
id: 'theme.SearchPage.emptyResultsTitle',
|
||||
|
|
@ -199,184 +68,148 @@ function getSearchPageTitle(searchQuery: string | undefined): string {
|
|||
|
||||
function SearchPageContent(): ReactNode {
|
||||
const {
|
||||
i18n: { currentLocale },
|
||||
} = useDocusaurusContext();
|
||||
const { appId, apiKey, indexName, contextualSearch } = useAlgoliaThemeConfig();
|
||||
const processSearchResultUrl = useSearchResultUrlProcessor();
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
items,
|
||||
totalResults,
|
||||
loading,
|
||||
loadingMore,
|
||||
error,
|
||||
hasMore,
|
||||
facets,
|
||||
refinements,
|
||||
toggleRefinement,
|
||||
clearRefinements,
|
||||
hasActiveRefinements,
|
||||
loadMore,
|
||||
sendResultClick,
|
||||
versionHelpers,
|
||||
} = useSearchPage();
|
||||
const { contextualSearch } = useAlgoliaThemeConfig();
|
||||
const documentsFoundPlural = useDocumentsFoundPlural();
|
||||
const {
|
||||
recentSearches,
|
||||
addRecentSearch,
|
||||
removeRecentSearch,
|
||||
clearRecentSearches,
|
||||
} = useRecentSearches();
|
||||
const topSections = useTopSections();
|
||||
|
||||
const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers();
|
||||
const [searchQuery, setSearchQuery] = useSearchQueryString();
|
||||
const pageTitle = getSearchPageTitle(searchQuery);
|
||||
const showVersionSelects =
|
||||
contextualSearch && versionHelpers.versioningEnabled;
|
||||
const hasSidebar = showVersionSelects || facets.length > 0;
|
||||
const isSearching = Boolean(searchQuery) || hasActiveRefinements;
|
||||
|
||||
const initialSearchResultState: ResultDispatcherState = {
|
||||
items: [],
|
||||
query: null,
|
||||
totalResults: null,
|
||||
totalPages: null,
|
||||
lastPage: null,
|
||||
hasMore: null,
|
||||
loading: null,
|
||||
const handleSelectResult = (
|
||||
item: SearchResultItem,
|
||||
position: number
|
||||
): void => {
|
||||
sendResultClick(item, position);
|
||||
if (searchQuery) {
|
||||
addRecentSearch(searchQuery);
|
||||
}
|
||||
};
|
||||
const [searchResultState, searchResultStateDispatcher] = useReducer(
|
||||
(prevState: ResultDispatcherState, data: ResultDispatcher) => {
|
||||
switch (data.type) {
|
||||
case 'reset': {
|
||||
return initialSearchResultState;
|
||||
}
|
||||
case 'loading': {
|
||||
return { ...prevState, loading: true };
|
||||
}
|
||||
case 'update': {
|
||||
if (searchQuery !== data.value.query) {
|
||||
return prevState;
|
||||
|
||||
function renderMainContent(): ReactNode {
|
||||
if (!isSearching) {
|
||||
return (
|
||||
<SearchEmptyState
|
||||
recentSearches={recentSearches}
|
||||
topSections={topSections}
|
||||
onSelectRecent={setSearchQuery}
|
||||
onRemoveRecent={removeRecentSearch}
|
||||
onClearRecent={clearRecentSearches}
|
||||
onSelectSection={(section) =>
|
||||
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) =>
|
||||
value.replace(/algolia-docsearch-suggestion--highlight/g, 'search-result-match');
|
||||
if (error) {
|
||||
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(
|
||||
({
|
||||
url,
|
||||
_highlightResult: { hierarchy },
|
||||
_snippetResult: snippet = {},
|
||||
}: {
|
||||
url: string;
|
||||
_highlightResult: { hierarchy: { [key: string]: { value: string } } };
|
||||
_snippetResult: { content?: { value: string } };
|
||||
}) => {
|
||||
const titles = Object.keys(hierarchy).map((key) => sanitizeValue(hierarchy[key]!.value));
|
||||
return {
|
||||
title: titles.pop()!,
|
||||
url: processSearchResultUrl(url),
|
||||
summary: snippet.content ? `${sanitizeValue(snippet.content.value)}...` : '',
|
||||
breadcrumbs: titles,
|
||||
};
|
||||
},
|
||||
if (loading && items.length === 0) {
|
||||
return <SearchResultsSkeleton />;
|
||||
}
|
||||
|
||||
if (items.length > 0) {
|
||||
return (
|
||||
<>
|
||||
<SearchResults items={items} onSelect={handleSelectResult} />
|
||||
{hasMore && (
|
||||
<div className={styles.loadMoreWrapper}>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(
|
||||
'button',
|
||||
'button--secondary',
|
||||
'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 (
|
||||
<Layout>
|
||||
|
|
@ -390,127 +223,62 @@ function SearchPageContent(): ReactNode {
|
|||
</Head>
|
||||
|
||||
<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()}>
|
||||
<div
|
||||
className={clsx('col', styles.searchQueryColumn, {
|
||||
'col--9': docsSearchVersionsHelpers.versioningEnabled,
|
||||
'col--12': !docsSearchVersionsHelpers.versioningEnabled,
|
||||
})}
|
||||
>
|
||||
<input
|
||||
autoFocus={true}
|
||||
type="search"
|
||||
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
|
||||
className={styles.searchForm}
|
||||
role="search"
|
||||
onSubmit={(event) => event.preventDefault()}
|
||||
>
|
||||
<SearchInput
|
||||
autoFocus={true}
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
onClear={() => setSearchQuery('')}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<div className="row">
|
||||
<div className={clsx('col', 'col--8', styles.searchResultsColumn)}>
|
||||
{searchResultState.totalResults !== null && documentsFoundPlural(searchResultState.totalResults)}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
styles.layout,
|
||||
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)}>
|
||||
<span>
|
||||
{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/"
|
||||
aria-label={translate({
|
||||
id: 'theme.SearchPage.algoliaLabel',
|
||||
message: 'Powered by Algolia',
|
||||
description: 'The description label for Algolia mention',
|
||||
})}
|
||||
>
|
||||
<AlgoliaLogo />
|
||||
</Link>
|
||||
<div className={styles.mainColumn}>
|
||||
{isSearching && (totalResults !== null || hasActiveRefinements) && (
|
||||
<div className={styles.resultsMeta}>
|
||||
{totalResults !== null && (
|
||||
<span className={styles.resultsCount}>
|
||||
{documentsFoundPlural(totalResults)}
|
||||
</span>
|
||||
)}
|
||||
<ActiveRefinements
|
||||
refinements={refinements}
|
||||
onRemove={toggleRefinement}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderMainContent()}
|
||||
</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>
|
||||
</Layout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
/* stylelint-disable selector-class-pattern -- CSS module class names are camelCased locals */
|
||||
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
|
|
@ -5,123 +7,680 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.searchQueryInput,
|
||||
.searchVersionInput {
|
||||
border-radius: var(--ifm-global-radius);
|
||||
border: 2px solid var(--ifm-toc-border-color);
|
||||
font: var(--ifm-font-size-base) var(--ifm-font-family-base);
|
||||
padding: 0.8rem;
|
||||
width: 100%;
|
||||
background: var(--docsearch-searchbox-focus-background);
|
||||
color: var(--docsearch-text-color);
|
||||
margin-bottom: 0.5rem;
|
||||
transition: border var(--ifm-transition-fast) ease;
|
||||
/* Header */
|
||||
|
||||
.pageHeader {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.searchQueryInput:focus,
|
||||
.searchVersionInput:focus {
|
||||
border-color: var(--docsearch-primary-color);
|
||||
outline: none;
|
||||
.pageTitle {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.searchQueryInput::placeholder {
|
||||
color: var(--docsearch-muted-color);
|
||||
}
|
||||
|
||||
.searchResultsColumn {
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.searchLogoColumn {
|
||||
.poweredBy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.searchLogoColumn a {
|
||||
.poweredByLabel {
|
||||
color: var(--ifm-color-content-secondary);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.poweredByLink {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.searchLogoColumn span {
|
||||
color: var(--docsearch-muted-color);
|
||||
font-weight: normal;
|
||||
.algoliaLogo {
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.searchResultItem {
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid var(--ifm-toc-border-color);
|
||||
/* Search box */
|
||||
|
||||
.searchForm {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.searchResultItemHeading {
|
||||
font-weight: 400;
|
||||
.searchBox {
|
||||
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;
|
||||
}
|
||||
|
||||
.searchResultItemPath {
|
||||
font-size: 0.8rem;
|
||||
.facetLabel {
|
||||
margin: 0 0 0.6rem;
|
||||
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 {
|
||||
margin: 0.5rem 0 0;
|
||||
font-style: italic;
|
||||
.facetList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 996px) {
|
||||
.searchQueryColumn {
|
||||
max-width: 60% !important;
|
||||
}
|
||||
|
||||
.searchVersionColumn {
|
||||
max-width: 40% !important;
|
||||
}
|
||||
|
||||
.searchResultsColumn {
|
||||
max-width: 60% !important;
|
||||
}
|
||||
|
||||
.searchLogoColumn {
|
||||
max-width: 40% !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
.facetItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.35rem 0.4rem;
|
||||
border-radius: var(--ifm-global-radius);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 576px) {
|
||||
.searchQueryColumn {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.searchVersionColumn {
|
||||
max-width: 100% !important;
|
||||
padding-left: var(--ifm-spacing-horizontal) !important;
|
||||
}
|
||||
.facetItem:hover {
|
||||
background: var(--ifm-color-emphasis-100);
|
||||
}
|
||||
|
||||
.loadingSpinner {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: 0.4em solid #eee;
|
||||
border-top-color: var(--ifm-color-primary);
|
||||
border-radius: 50%;
|
||||
animation: loading-spin 1s linear infinite;
|
||||
margin: 0 auto;
|
||||
.facetCheckbox {
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
accent-color: var(--ifm-color-primary);
|
||||
}
|
||||
|
||||
@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% {
|
||||
transform: rotate(360deg);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin-top: 2rem;
|
||||
/* Empty state */
|
||||
|
||||
.emptyState {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
padding-block: 1rem;
|
||||
}
|
||||
|
||||
:global(.search-result-match) {
|
||||
color: var(--docsearch-hit-color);
|
||||
background: rgb(255 215 142 / 25%);
|
||||
padding: 0.09em 0;
|
||||
.emptySectionHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import type { DocSearchTranslations } from '@docsearch/react';
|
||||
|
|
@ -39,6 +39,7 @@ const translations: DocSearchTranslations & {
|
|||
askAiScreen: {
|
||||
disclaimerText: string;
|
||||
relatedSourcesText: string;
|
||||
relatedSourcesTextPlural: string;
|
||||
thinkingText: string;
|
||||
copyButtonText: string;
|
||||
copyButtonCopiedText: string;
|
||||
|
|
@ -46,6 +47,17 @@ const translations: DocSearchTranslations & {
|
|||
likeButtonTitle: string;
|
||||
dislikeButtonTitle: 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;
|
||||
duringToolCallText: string;
|
||||
afterToolCallText: string;
|
||||
|
|
@ -127,7 +139,8 @@ const translations: DocSearchTranslations & {
|
|||
placeholderTextAskAiStreaming: translate({
|
||||
id: 'theme.SearchModal.searchBox.placeholderTextAskAiStreaming',
|
||||
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({
|
||||
id: 'theme.SearchModal.searchBox.enterKeyHint',
|
||||
|
|
@ -219,13 +232,19 @@ const translations: DocSearchTranslations & {
|
|||
askAiScreen: {
|
||||
disclaimerText: translate({
|
||||
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',
|
||||
}),
|
||||
relatedSourcesText: translate({
|
||||
id: 'theme.SearchModal.askAiScreen.relatedSourcesText',
|
||||
message: 'Related sources',
|
||||
description: 'The text for related sources',
|
||||
message: 'Source',
|
||||
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({
|
||||
id: 'theme.SearchModal.askAiScreen.thinkingText',
|
||||
|
|
@ -262,6 +281,67 @@ const translations: DocSearchTranslations & {
|
|||
message: 'Thanks for your 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({
|
||||
id: 'theme.SearchModal.askAiScreen.preToolCallText',
|
||||
message: 'Searching...',
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
* 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.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/// <reference types="@docusaurus/module-type-aliases" />
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
/**
|
||||
* 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.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Escapes special characters in a string for use in a regular expression.
|
||||
* Based on escape-string-regexp package.
|
||||
* Escapes special characters in a string for use in a regular expression. Based
|
||||
* on escape-string-regexp package.
|
||||
*/
|
||||
export function escapeRegexp(string: string): string {
|
||||
// Escape characters with special meaning either inside or outside character
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
* 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.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
export { normalizeUrl } from './normalizeUrl';
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
* 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.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -10,7 +10,8 @@
|
|||
* joins them into a reasonable URL.
|
||||
*
|
||||
* - `["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.
|
||||
* - Replace `?` in query parameters with `&`.
|
||||
* - Dedupe forward slashes in the entire path, avoiding protocol slashes.
|
||||
|
|
@ -24,7 +25,8 @@ export function normalizeUrl(rawUrls: string[]): string {
|
|||
let hasStartingSlash = 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)) {
|
||||
return '';
|
||||
|
|
@ -69,7 +71,7 @@ export function normalizeUrl(rawUrls: string[]): string {
|
|||
/^\/+/,
|
||||
// Special case where the first element of rawUrls is empty
|
||||
// ["", "/hello"] => /hello
|
||||
component.startsWith('/') && !hasStartingSlash ? '/' : '',
|
||||
component.startsWith('/') && !hasStartingSlash ? '/' : ''
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,39 +1,50 @@
|
|||
/**
|
||||
* 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.
|
||||
* This source code is licensed under the MIT license found in the LICENSE file
|
||||
* 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 Joi from 'joi';
|
||||
|
||||
import { docSearchVersionString } from './docSearchVersion';
|
||||
import { getDocSearchConfig } from './getDocSearchConfig';
|
||||
import { escapeRegexp } from './utils';
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
// Enabled by default, as it makes sense in most cases
|
||||
// see also https://github.com/facebook/docusaurus/issues/5880
|
||||
contextualSearch: true,
|
||||
searchParameters: {},
|
||||
searchPagePath: 'search',
|
||||
} satisfies Partial<ThemeConfigAlgolia>;
|
||||
searchPage: {
|
||||
path: 'search',
|
||||
},
|
||||
} 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(),
|
||||
filters: Joi.string().optional(),
|
||||
attributesToRetrieve: 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();
|
||||
|
||||
const AskAiMemorySchema = Joi.object({
|
||||
enabled: Joi.bool().optional().default(false),
|
||||
userToken: Joi.string().optional(),
|
||||
}).unknown(false);
|
||||
|
||||
const SidePanelKeyboardShortcutsSchema = Joi.object({
|
||||
'Ctrl/Cmd+I': Joi.boolean().optional(),
|
||||
});
|
||||
}).unknown(false);
|
||||
|
||||
const SidePanelSchema = Joi.object({
|
||||
keyboardShortcuts: SidePanelKeyboardShortcutsSchema.optional(),
|
||||
|
|
@ -45,179 +56,220 @@ const SidePanelSchema = Joi.object({
|
|||
suggestedQuestions: Joi.boolean().optional(),
|
||||
translations: Joi.object().optional().unknown(),
|
||||
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>({
|
||||
algolia: Joi.object<ThemeConfigAlgolia>({
|
||||
// Docusaurus attributes
|
||||
contextualSearch: Joi.boolean().default(DEFAULT_CONFIG.contextualSearch),
|
||||
externalUrlRegex: Joi.string().optional(),
|
||||
// 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'];
|
||||
}
|
||||
const KeyboardShortcutsSchema = Joi.object({
|
||||
'Ctrl/Cmd+K': Joi.boolean().optional(),
|
||||
'/': Joi.boolean().optional(),
|
||||
'Ctrl/Cmd+I': Joi.boolean().optional(),
|
||||
}).unknown(false);
|
||||
|
||||
// Fill in missing fields with the top-level Algolia config
|
||||
const normalizedAskAi = { ...askAiInput };
|
||||
normalizedAskAi.indexName = normalizedAskAi.indexName ?? algolia.indexName;
|
||||
normalizedAskAi.apiKey = normalizedAskAi.apiKey ?? algolia.apiKey;
|
||||
normalizedAskAi.appId = normalizedAskAi.appId ?? algolia.appId;
|
||||
if (
|
||||
normalizedAskAi.agentStudio !== true &&
|
||||
normalizedAskAi.searchParameters?.facetFilters === undefined &&
|
||||
algoliaFacetFilters
|
||||
) {
|
||||
normalizedAskAi.searchParameters = {
|
||||
...(normalizedAskAi.searchParameters ?? {}),
|
||||
facetFilters: algoliaFacetFilters,
|
||||
};
|
||||
}
|
||||
const IndexSchema = Joi.alternatives().try(
|
||||
Joi.string(),
|
||||
Joi.object({
|
||||
name: Joi.string().required(),
|
||||
searchParameters: SearchParametersSchema.optional(),
|
||||
}).unknown(false)
|
||||
);
|
||||
|
||||
return normalizedAskAi;
|
||||
})
|
||||
.optional()
|
||||
.messages({
|
||||
'alternatives.types':
|
||||
'askAi must be either a string (assistantId) or an object with indexName, apiKey, appId, and assistantId',
|
||||
}),
|
||||
const AskAiPromptSuggestionsSchema = Joi.object({
|
||||
indexName: Joi.string().min(1).required(),
|
||||
hitsPerPage: Joi.number().positive().optional().default(3),
|
||||
}).unknown(false);
|
||||
|
||||
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')
|
||||
.required()
|
||||
.unknown(),
|
||||
});
|
||||
.unknown(false)
|
||||
.optional(),
|
||||
searchPage: SearchPageSchema,
|
||||
askAi: AskAiSchema.optional(),
|
||||
sidePanel: Joi.alternatives().try(Joi.boolean(), SidePanelSchema).optional(),
|
||||
})
|
||||
.label('themeConfig.docsearch')
|
||||
.unknown(false);
|
||||
|
||||
function ensureSidepanelSupported(themeConfig: ThemeConfig) {
|
||||
const docsearch = getDocSearchConfig(themeConfig);
|
||||
const sidePanelEnabled = docsearch.askAi && typeof docsearch.askAi === 'object' && Boolean(docsearch.askAi.sidePanel);
|
||||
const Schema = Joi.object<ThemeConfig>({
|
||||
docsearch: DocSearchSchema.required(),
|
||||
}).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;
|
||||
}
|
||||
|
||||
const isSidepanelSupported = (() => {
|
||||
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);
|
||||
})();
|
||||
const docsearchRecord = docsearch as Record<string, unknown>;
|
||||
|
||||
if (!isSidepanelSupported) {
|
||||
if (docsearchRecord.indexName !== undefined) {
|
||||
throw new Error(
|
||||
'The askAi.sidePanel feature is only supported in DocSearch v4.5+. ' +
|
||||
'Please upgrade to DocSearch v4.5+ or remove the askAi.sidePanel configuration.',
|
||||
'`themeConfig.docsearch.indexName` was removed. Use `themeConfig.docsearch.indices` instead.'
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
return value !== undefined && value !== null;
|
||||
}
|
||||
function ensureSidePanelHasAskAi(themeConfig: ThemeConfig): void {
|
||||
const { docsearch } = themeConfig;
|
||||
|
||||
function getThemeConfigSource(themeConfig: ThemeConfig): 'algolia' | 'docsearch' | null {
|
||||
const hasDocsearch = hasConfigValue(themeConfig.docsearch);
|
||||
const hasAlgolia = hasConfigValue(themeConfig.algolia);
|
||||
|
||||
if (hasDocsearch && hasAlgolia) {
|
||||
if (docsearch?.sidePanel && !docsearch.askAi) {
|
||||
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({
|
||||
validate,
|
||||
themeConfig: themeConfigInput,
|
||||
}: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
|
||||
const source = getThemeConfigSource(themeConfigInput);
|
||||
assertNoRemovedKeys(themeConfigInput);
|
||||
|
||||
if (!source) {
|
||||
return validate(Schema, {});
|
||||
}
|
||||
|
||||
const validated = validate(Schema, {
|
||||
algolia: source === 'docsearch' ? themeConfigInput.docsearch : themeConfigInput.algolia,
|
||||
const themeConfig = validate(Schema, {
|
||||
docsearch: themeConfigInput.docsearch,
|
||||
}) as ThemeConfig;
|
||||
|
||||
const themeConfig: ThemeConfig = {
|
||||
[source]: validated.algolia,
|
||||
};
|
||||
|
||||
ensureSidepanelSupported(themeConfig);
|
||||
ensureSidePanelHasAskAi(themeConfig);
|
||||
return themeConfig;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
"rootDir": "src",
|
||||
"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__/**"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"references": [{"path": "./tsconfig.client.json"}],
|
||||
"references": [{ "path": "./tsconfig.client.json" }],
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"outDir": "lib",
|
||||
|
|
|
|||
|
|
@ -2,15 +2,19 @@
|
|||
"files": [
|
||||
{
|
||||
"path": "packages/docsearch-css/dist/style.css",
|
||||
"maxSize": "6 kB"
|
||||
"maxSize": "8.2 kB"
|
||||
},
|
||||
{
|
||||
"path": "packages/docsearch-react/dist/umd/index.js",
|
||||
"maxSize": "115 kB"
|
||||
"maxSize": "160 kB"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
|
|
@ -19,6 +23,14 @@
|
|||
{
|
||||
"path": "packages/docsearch-modal/dist/umd/index.js",
|
||||
"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