From 5737b5781c44c5a2528437969cc840db94f96252 Mon Sep 17 00:00:00 2001 From: Niall O'Brien Date: Thu, 26 Mar 2026 15:48:44 +0000 Subject: [PATCH 01/25] feat: initial commit of Algolia search integration --- .env | 12 +- .gitignore | 4 +- package-lock.json | 531 +++++++++++++++++- package.json | 3 + .../design-system/page.module.css | 24 +- .../SearchDialog/FilterButtons.module.css | 61 -- src/components/SearchDialog/FilterButtons.tsx | 32 -- src/components/SearchDialog/SearchDialog.jsx | 303 ++++++++++ .../SearchDialog/SearchDialog.module.css | 201 ++----- src/components/SearchDialog/SearchDialog.tsx | 227 -------- .../SearchDialogContent.jsx | 90 +++ .../SearchDialogContent.module.css | 137 +++++ .../SearchFilters/SearchFilters.jsx | 69 +++ .../SearchFilters/SearchFilters.module.css | 68 +++ .../SearchFilters/index.js | 3 + .../SearchResults/SearchResults.jsx | 295 ++++++++++ .../SearchResults/SearchResults.module.css | 152 +++++ .../SearchResults/index.js | 3 + .../SearchDialog/SearchDialogContent/index.js | 3 + .../SearchDialog/SearchForm.module.css | 19 - src/components/SearchDialog/SearchForm.tsx | 61 -- .../SearchDialog/SearchTrigger.module.css | 74 --- src/components/SearchDialog/SearchTrigger.tsx | 35 -- .../SearchTrigger/SearchTrigger.jsx | 15 + .../SearchTrigger/SearchTrigger.module.css | 43 ++ .../SearchDialog/SearchTrigger/index.js | 3 + src/components/SearchDialog/index.js | 3 + src/components/SearchDialog/index.ts | 3 - src/components/SearchDialog/utils/data.js | 49 ++ .../SearchDialog/utils/searchGroupUtils.js | 182 ++++++ .../utils/searchGroupUtils.test.js | 107 ++++ .../SearchDialog/utils/searchHitUtils.js | 55 ++ .../WithQuickNav/WithQuicknav.module.css | 2 +- src/icons/svgs/External.tsx | 4 +- src/util/strings.js | 42 ++ src/util/url.js | 26 + 36 files changed, 2243 insertions(+), 698 deletions(-) delete mode 100644 src/components/SearchDialog/FilterButtons.module.css delete mode 100644 src/components/SearchDialog/FilterButtons.tsx create mode 100644 src/components/SearchDialog/SearchDialog.jsx delete mode 100644 src/components/SearchDialog/SearchDialog.tsx create mode 100644 src/components/SearchDialog/SearchDialogContent/SearchDialogContent.jsx create mode 100644 src/components/SearchDialog/SearchDialogContent/SearchDialogContent.module.css create mode 100644 src/components/SearchDialog/SearchDialogContent/SearchFilters/SearchFilters.jsx create mode 100644 src/components/SearchDialog/SearchDialogContent/SearchFilters/SearchFilters.module.css create mode 100644 src/components/SearchDialog/SearchDialogContent/SearchFilters/index.js create mode 100644 src/components/SearchDialog/SearchDialogContent/SearchResults/SearchResults.jsx create mode 100644 src/components/SearchDialog/SearchDialogContent/SearchResults/SearchResults.module.css create mode 100644 src/components/SearchDialog/SearchDialogContent/SearchResults/index.js create mode 100644 src/components/SearchDialog/SearchDialogContent/index.js delete mode 100644 src/components/SearchDialog/SearchForm.module.css delete mode 100644 src/components/SearchDialog/SearchForm.tsx delete mode 100644 src/components/SearchDialog/SearchTrigger.module.css delete mode 100644 src/components/SearchDialog/SearchTrigger.tsx create mode 100644 src/components/SearchDialog/SearchTrigger/SearchTrigger.jsx create mode 100644 src/components/SearchDialog/SearchTrigger/SearchTrigger.module.css create mode 100644 src/components/SearchDialog/SearchTrigger/index.js create mode 100644 src/components/SearchDialog/index.js delete mode 100644 src/components/SearchDialog/index.ts create mode 100644 src/components/SearchDialog/utils/data.js create mode 100644 src/components/SearchDialog/utils/searchGroupUtils.js create mode 100644 src/components/SearchDialog/utils/searchGroupUtils.test.js create mode 100644 src/components/SearchDialog/utils/searchHitUtils.js create mode 100644 src/util/strings.js create mode 100644 src/util/url.js diff --git a/.env b/.env index 28f19231..a6a63c74 100644 --- a/.env +++ b/.env @@ -2,4 +2,14 @@ NEXT_PUBLIC_CLOUDSMITH_API_URL="https://api.cloudsmith.io" NEXT_PUBLIC_CLOUDSMITH_DOCS_URL="https://github.com/cloudsmith-io/cloudsmith-docs" NEXT_PUBLIC_CLOUDSMITH_DOCS_BRANCH="main" CLOUDSMITH_API_V1_URL="https://api.cloudsmith.io/swagger/?format=openapi" -CLOUDSMITH_API_V2_URL="https://api.cloudsmith.io/v2/openapi/?format=json" \ No newline at end of file +CLOUDSMITH_API_V2_URL="https://api.cloudsmith.io/v2/openapi/?format=json" + +NEXT_PUBLIC_ALGOLIA_APP_ID= +NEXT_PUBLIC_ALGOLIA_API_KEY=77bed0e1d7c9a7b0572d1ffdb55b2be9 # this is only for development, a restricted key is used in production +NEXT_PUBLIC_ALGOLIA_DOCS_INDEX=Docs +NEXT_PUBLIC_ALGOLIA_INDEX=prod_WEBSITE +# NEXT_PUBLIC_ALGOLIA_INDEX=dev_WEBSITE + +ALGOLIA_WRITE_KEY= +NEXT_PUBLIC_DOCS_SITE_ORIGIN=https://docs.cloudsmith.com +NEXT_PUBLIC_MARKETING_SITE_ORIGIN=https://cloudsmith.com \ No newline at end of file diff --git a/.gitignore b/.gitignore index f6506a7f..bd5c0e1c 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,6 @@ npm-debug.log* *.tsbuildinfo next-env.d.ts -docs/graph.html \ No newline at end of file +docs/graph.html + +.history \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e238bd8b..eb626e94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@mdx-js/react": "^3.1.0", "@next/mdx": "15.1.6", "@vercel/analytics": "^1.6.1", + "algoliasearch": "^5.50.0", "class-variance-authority": "^0.7.1", "date-fns": "^4.1.0", "fast-fuzzy": "^1.12.0", @@ -23,6 +24,8 @@ "react-dom": "19.2.1", "react-error-boundary": "^6.1.0", "react-hotkeys-hook": "^4.6.1", + "react-instantsearch": "^7.28.1", + "react-instantsearch-nextjs": "^1.1.3", "react-player": "^2.16.0", "remark-frontmatter": "^5.0.0", "remark-mdx-frontmatter": "^5.0.0" @@ -82,6 +85,207 @@ "dev": true, "license": "MIT" }, + "node_modules/@algolia/abtesting": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.16.0.tgz", + "integrity": "sha512-alHFZ68/i9qLC/muEB07VQ9r7cB8AvCcGX6dVQi2PNHhc/ZQRmmFAv8KK1ay4UiseGSFr7f0nXBKsZ/jRg7e4g==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.0", + "@algolia/requester-browser-xhr": "5.50.0", + "@algolia/requester-fetch": "5.50.0", + "@algolia/requester-node-http": "5.50.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.50.0.tgz", + "integrity": "sha512-mfgUdLQNxOAvCZUGzPQxjahEWEPuQkKlV0ZtGmePOa9ZxIQZlk31vRBNbM6ScU8jTH41SCYE77G/lCifDr1SVw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.0", + "@algolia/requester-browser-xhr": "5.50.0", + "@algolia/requester-fetch": "5.50.0", + "@algolia/requester-node-http": "5.50.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.50.0.tgz", + "integrity": "sha512-5mjokeKYyPaP3Q8IYJEnutI+O4dW/Ixxx5IgsSxT04pCfGqPXxTOH311hTQxyNpcGGEOGrMv8n8Z+UMTPamioQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.0", + "@algolia/requester-browser-xhr": "5.50.0", + "@algolia/requester-fetch": "5.50.0", + "@algolia/requester-node-http": "5.50.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.50.0.tgz", + "integrity": "sha512-emtOvR6dl3rX3sBJXXbofMNHU1qMQqQSWu319RMrNL5BWoBqyiq7y0Zn6cjJm7aGHV/Qbf+KCCYeWNKEMPI3BQ==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.50.0.tgz", + "integrity": "sha512-IerGH2/hcj/6bwkpQg/HHRqmlGN1XwygQWythAk0gZFBrghs9danJaYuSS3ShzLSVoIVth4jY5GDPX9Lbw5cgg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.0", + "@algolia/requester-browser-xhr": "5.50.0", + "@algolia/requester-fetch": "5.50.0", + "@algolia/requester-node-http": "5.50.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.50.0.tgz", + "integrity": "sha512-3idPJeXn5L0MmgP9jk9JJqblrQ/SguN93dNK9z9gfgyupBhHnJMOEjrRYcVgTIfvG13Y04wO+Q0FxE2Ut8PVbA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.0", + "@algolia/requester-browser-xhr": "5.50.0", + "@algolia/requester-fetch": "5.50.0", + "@algolia/requester-node-http": "5.50.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.50.0.tgz", + "integrity": "sha512-q7qRoWrQK1a8m5EFQEmPlo7+pg9mVQ8X5jsChtChERre0uS2pdYEDixBBl0ydBSGkdGbLUDufcACIhH/077E4g==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.0", + "@algolia/requester-browser-xhr": "5.50.0", + "@algolia/requester-fetch": "5.50.0", + "@algolia/requester-node-http": "5.50.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.50.0.tgz", + "integrity": "sha512-Jc360x4yqb3eEg4OY4KEIdGePBxZogivKI+OGIU8aLXgAYPTECvzeOBc90312yHA1hr3AeRlAFl0rIc8lQaIrQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.0", + "@algolia/requester-browser-xhr": "5.50.0", + "@algolia/requester-fetch": "5.50.0", + "@algolia/requester-node-http": "5.50.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/events": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz", + "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==", + "license": "MIT" + }, + "node_modules/@algolia/ingestion": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.50.0.tgz", + "integrity": "sha512-OS3/Viao+NPpyBbEY3tf6hLewppG+UclD+9i0ju56mq2DrdMJFCkEky6Sk9S5VPcbLzxzg3BqBX6u9Q35w19aQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.0", + "@algolia/requester-browser-xhr": "5.50.0", + "@algolia/requester-fetch": "5.50.0", + "@algolia/requester-node-http": "5.50.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.50.0.tgz", + "integrity": "sha512-/znwgSiGufpbJVIoDmeQaHtTq+OMdDawFRbMSJVv+12n79hW+qdQXS8/Uu3BD3yn0BzgVFJEvrsHrCsInZKdhw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.0", + "@algolia/requester-browser-xhr": "5.50.0", + "@algolia/requester-fetch": "5.50.0", + "@algolia/requester-node-http": "5.50.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.50.0.tgz", + "integrity": "sha512-dHjUfu4jfjdQiKDpCpAnM7LP5yfG0oNShtfpF5rMCel6/4HIoqJ4DC4h5GKDzgrvJYtgAhblo0AYBmOM00T+lQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.0", + "@algolia/requester-browser-xhr": "5.50.0", + "@algolia/requester-fetch": "5.50.0", + "@algolia/requester-node-http": "5.50.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.50.0.tgz", + "integrity": "sha512-bffIbUljAWnh/Ctu5uScORajuUavqmZ0ACYd1fQQeSSYA9NNN83ynO26pSc2dZRXpSK0fkc1//qSSFXMKGu+aw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.50.0.tgz", + "integrity": "sha512-y0EwNvPGvkM+yTAqqO6Gpt9wVGm3CLDtpLvNEiB3VGvN3WzfkjZGtLUsG/ru2kVJIIU7QcV0puuYgEpBeFxcJg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.50.0.tgz", + "integrity": "sha512-xpwefe4fCOWnZgXCbkGpqQY6jgBSCf2hmgnySbyzZIccrv3SoashHKGPE4x6vVG+gdHrGciMTAcDo9HOZwH22Q==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -7430,6 +7634,12 @@ "@types/ms": "*" } }, + "node_modules/@types/dom-speech-recognition": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.1.tgz", + "integrity": "sha512-udCxb8DvjcDKfk1WTBzDsxFbLgYxmQGKrE/ricoMqHRNjSlSUCcamVTA5lIQqzY10mY5qCY0QDwBfFEwhfoDPw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -7445,6 +7655,12 @@ "@types/estree": "*" } }, + "node_modules/@types/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -7464,6 +7680,12 @@ "@types/unist": "*" } }, + "node_modules/@types/hogan.js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hogan.js/-/hogan.js-3.0.5.tgz", + "integrity": "sha512-/uRaY3HGPWyLqOyhgvW9Aa43BNnLZrNeQxl2p8wqId4UHMfPKolSB+U7BlZyO1ng7MkLnyEAItsBzCG0SDhqrA==", + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -7593,6 +7815,12 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.0.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", @@ -8133,6 +8361,12 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -8221,6 +8455,43 @@ } } }, + "node_modules/algoliasearch": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.50.0.tgz", + "integrity": "sha512-yE5I83Q2s8euVou8Y3feXK08wyZInJWLYXgWO6Xti9jBUEZAGUahyeQ7wSZWkifLWVnQVKEz5RAmBlXG5nqxog==", + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.16.0", + "@algolia/client-abtesting": "5.50.0", + "@algolia/client-analytics": "5.50.0", + "@algolia/client-common": "5.50.0", + "@algolia/client-insights": "5.50.0", + "@algolia/client-personalization": "5.50.0", + "@algolia/client-query-suggestions": "5.50.0", + "@algolia/client-search": "5.50.0", + "@algolia/ingestion": "1.50.0", + "@algolia/monitoring": "1.50.0", + "@algolia/recommend": "5.50.0", + "@algolia/requester-browser-xhr": "5.50.0", + "@algolia/requester-fetch": "5.50.0", + "@algolia/requester-node-http": "5.50.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/algoliasearch-helper": { + "version": "3.28.1", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.28.1.tgz", + "integrity": "sha512-6iXpbkkrAI5HFpCWXlNmIDSBuoN/U1XnEvb2yJAoWfqrZ+DrybI7MQ5P5mthFaprmocq+zbi6HxnR28xnZAYBw==", + "license": "MIT", + "dependencies": { + "@algolia/events": "^4.0.1" + }, + "peerDependencies": { + "algoliasearch": ">= 3.1 < 6" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -8943,7 +9214,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -8957,7 +9227,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -9975,7 +10244,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -10123,7 +10391,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10133,7 +10400,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10171,7 +10437,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -11282,7 +11547,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11343,7 +11607,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -11388,7 +11651,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -11584,7 +11846,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11673,7 +11934,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11702,7 +11962,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -11861,6 +12120,18 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hogan.js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/hogan.js/-/hogan.js-3.0.2.tgz", + "integrity": "sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==", + "dependencies": { + "mkdirp": "0.3.0", + "nopt": "1.0.10" + }, + "bin": { + "hulk": "bin/hulk" + } + }, "node_modules/hookified": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.8.1.tgz", @@ -11868,6 +12139,12 @@ "dev": true, "license": "MIT" }, + "node_modules/htm": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz", + "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==", + "license": "Apache-2.0" + }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -12091,6 +12368,63 @@ "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", "license": "MIT" }, + "node_modules/instantsearch-ui-components": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/instantsearch-ui-components/-/instantsearch-ui-components-0.22.1.tgz", + "integrity": "sha512-pHao8HrIeUYicgBX27UkzNLPd1JVYvL2XKLF4aeg0X3ZQNhwJui7jUBbgfGSNcT/ILc26NTwx3NV5/q3Cd04wQ==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "0.5.18", + "markdown-to-jsx": "^7.7.15", + "zod": "^3.25.76 || ^4", + "zod-to-json-schema": "3.24.6" + } + }, + "node_modules/instantsearch-ui-components/node_modules/@swc/helpers": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", + "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/instantsearch.js": { + "version": "4.92.1", + "resolved": "https://registry.npmjs.org/instantsearch.js/-/instantsearch.js-4.92.1.tgz", + "integrity": "sha512-cHDpkum81iZ6+D9dw133E8N/cSckrv3WCawO76yhHMZiS7Ak1hQGiclG6U+Gek1U9y5T2R/+guvNqKguyvVUqg==", + "license": "MIT", + "dependencies": { + "@algolia/events": "^4.0.1", + "@swc/helpers": "0.5.18", + "@types/dom-speech-recognition": "^0.0.1", + "@types/google.maps": "^3.55.12", + "@types/hogan.js": "^3.0.0", + "@types/qs": "^6.5.3", + "algoliasearch-helper": "3.28.1", + "hogan.js": "^3.0.2", + "htm": "^3.0.0", + "instantsearch-ui-components": "0.22.1", + "preact": "^10.10.0", + "qs": "^6.5.1", + "react": ">= 0.14.0", + "search-insights": "^2.17.2", + "zod": "^3.25.76 || ^4", + "zod-to-json-schema": "3.24.6" + }, + "peerDependencies": { + "algoliasearch": ">= 3.1 < 6" + } + }, + "node_modules/instantsearch.js/node_modules/@swc/helpers": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", + "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -14080,11 +14414,27 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/markdown-to-jsx": { + "version": "7.7.17", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.17.tgz", + "integrity": "sha512-7mG/1feQ0TX5I7YyMZVDgCC/y2I3CiEhIRQIhyov9nGBP5eoVrOXXHuL5ZP8GRfxVZKRiXWJgwXkb9It+nQZfQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "react": ">= 0.14.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -15299,6 +15649,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/motion": { "version": "12.6.2", "resolved": "https://registry.npmjs.org/motion/-/motion-12.6.2.tgz", @@ -15552,6 +15912,21 @@ "dev": true, "license": "MIT" }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "license": "MIT", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -15723,7 +16098,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -17028,6 +17402,16 @@ "dev": true, "license": "MIT" }, + "node_modules/preact": { + "version": "10.29.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.0.tgz", + "integrity": "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -17163,6 +17547,21 @@ ], "license": "MIT" }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -17236,6 +17635,81 @@ "react-dom": ">=16.8.1" } }, + "node_modules/react-instantsearch": { + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/react-instantsearch/-/react-instantsearch-7.28.1.tgz", + "integrity": "sha512-nO4p+GbEu3KCofJ6idk2B1na2pvRdwb5nk4uBRbeewa21lKrNIbNq0kFvSMdXbSXQ7/o3fRHW/fIRdQem6X8rQ==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "0.5.18", + "instantsearch-ui-components": "0.22.1", + "instantsearch.js": "4.92.1", + "react-instantsearch-core": "7.28.1" + }, + "peerDependencies": { + "algoliasearch": ">= 3.1 < 6", + "react": ">= 16.8.0 < 20", + "react-dom": ">= 16.8.0 < 20" + } + }, + "node_modules/react-instantsearch-core": { + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/react-instantsearch-core/-/react-instantsearch-core-7.28.1.tgz", + "integrity": "sha512-1cAdxHYS/D4t0KqGQkNMb36O62fhHZczmECWy+99kpUA5qFZ1Mao7LQSVqYihdC8twLXAIMPr2rj8ZhvQ0CSGA==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "0.5.18", + "algoliasearch-helper": "3.28.1", + "instantsearch.js": "4.92.1", + "use-sync-external-store": "^1.0.0", + "zod": "^3.25.76 || ^4", + "zod-to-json-schema": "3.24.6" + }, + "peerDependencies": { + "algoliasearch": ">= 3.1 < 6", + "react": ">= 16.8.0 < 20" + } + }, + "node_modules/react-instantsearch-core/node_modules/@swc/helpers": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", + "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/react-instantsearch-nextjs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/react-instantsearch-nextjs/-/react-instantsearch-nextjs-1.1.3.tgz", + "integrity": "sha512-hgeHDqUan+NCM+gT30csFTrjCdu7BrvDyv0J4k6BcCkTEJ7FpcnUZ6Vj9UCKDaP0KFNKK4PamTokL5h4hwlwsQ==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "0.5.18" + }, + "peerDependencies": { + "next": ">= 13.4 < 17", + "react-instantsearch": ">= 7.1.0 < 8" + } + }, + "node_modules/react-instantsearch-nextjs/node_modules/@swc/helpers": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", + "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/react-instantsearch/node_modules/@swc/helpers": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", + "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -17967,6 +18441,12 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "license": "MIT" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -18188,7 +18668,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -18208,7 +18687,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -18225,7 +18703,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -18244,7 +18721,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -19810,7 +20286,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "dev": true, "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -20217,6 +20692,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 351a23ba..bce3973e 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@mdx-js/react": "^3.1.0", "@next/mdx": "15.1.6", "@vercel/analytics": "^1.6.1", + "algoliasearch": "^5.50.0", "class-variance-authority": "^0.7.1", "date-fns": "^4.1.0", "fast-fuzzy": "^1.12.0", @@ -33,6 +34,8 @@ "react-dom": "19.2.1", "react-error-boundary": "^6.1.0", "react-hotkeys-hook": "^4.6.1", + "react-instantsearch": "^7.28.1", + "react-instantsearch-nextjs": "^1.1.3", "react-player": "^2.16.0", "remark-frontmatter": "^5.0.0", "remark-mdx-frontmatter": "^5.0.0" diff --git a/src/app/(documentation)/design-system/page.module.css b/src/app/(documentation)/design-system/page.module.css index 4ead224a..dc408106 100644 --- a/src/app/(documentation)/design-system/page.module.css +++ b/src/app/(documentation)/design-system/page.module.css @@ -21,17 +21,6 @@ color: var(--color-text-primary); } -.iconWrapper:hover { - border-color: var(--color-accent-default); - background: var(--color-accent-default); -} - -.iconWrapper:hover header span, -.iconWrapper:hover .logo, -.iconWrapper:hover .arrowIcon { - color: var(--brand-color-white); -} - .iconWrapper footer { display: flex; align-items: end; @@ -58,7 +47,18 @@ font-size: 1.3em; } -@media (--medium) { +.iconWrapper:hover { + border-color: var(--color-accent-default); + background: var(--color-accent-default); +} + +.iconWrapper:hover header span, +.iconWrapper:hover .logo, +.iconWrapper:hover .arrowIcon { + color: var(--brand-color-white); +} + +@media (--tablet-up) { .logo { --logo-max-width: 40px; --logo-max-height: 20px; diff --git a/src/components/SearchDialog/FilterButtons.module.css b/src/components/SearchDialog/FilterButtons.module.css deleted file mode 100644 index cd9d96ca..00000000 --- a/src/components/SearchDialog/FilterButtons.module.css +++ /dev/null @@ -1,61 +0,0 @@ -.filters { - display: flex; - flex-direction: column; - gap: var(--space-3xs); - margin-block-end: var(--space-s); -} - -.filtersHeadline { - font-size: var(--text-body-2xs); - font-weight: var(--font-weight-bold); - text-transform: uppercase; - color: var(--base-color-grey-900); - line-height: var(--line-height-s); -} - -.filtersList { - display: flex; - flex-wrap: wrap; - gap: var(--space-2xs); -} - -.filter { - appearance: none; - display: inline-flex; - gap: var(--space-2xs); - justify-content: center; - align-items: center; - padding-block: var(--space-3xs); - padding-inline: var(--space-xs); - border-radius: var(--border-radius-m); - border: 1px solid var(--brand-color-grey-3); - background-color: transparent; - color: var(--base-color-grey-900); - font-size: var(--text-body-s); - white-space: nowrap; - - &:not(.filterDisabled) { - cursor: pointer; - - &:is(:hover, :focus) { - background-color: var(--brand-color-grey-2); - outline: none; - } - } -} - -.filterActive { - background-color: var(--base-color-grey-1000); - color: var(--base-color-white); - border: none; - - &:not(.filterDisabled):is(:hover, :focus) { - background-color: var(--base-color-grey-700); - } -} - -.filterIcon { - flex-shrink: 0; - width: 16px; - height: 16px; -} diff --git a/src/components/SearchDialog/FilterButtons.tsx b/src/components/SearchDialog/FilterButtons.tsx deleted file mode 100644 index 039551e6..00000000 --- a/src/components/SearchDialog/FilterButtons.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Icon } from '@/icons'; -import { cx } from 'class-variance-authority'; -import { Filters } from './SearchDialog'; - -import styles from './FilterButtons.module.css'; - -export interface FilterButtonsProps { - filters: Filters; - activeSections: Array; - onFilterChange: (filterId: string) => void; -} - -export const FilterButtons = ({ filters, activeSections, onFilterChange }: FilterButtonsProps) => { - return ( -
-
- {filters.map((filter) => ( - - ))} -
-
- ); -}; diff --git a/src/components/SearchDialog/SearchDialog.jsx b/src/components/SearchDialog/SearchDialog.jsx new file mode 100644 index 00000000..eaa2f46a --- /dev/null +++ b/src/components/SearchDialog/SearchDialog.jsx @@ -0,0 +1,303 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; + +import * as RadixDialog from '@radix-ui/react-dialog'; +import { VisuallyHidden } from '@radix-ui/react-visually-hidden'; +import { algoliasearch } from 'algoliasearch'; +import { cx } from 'class-variance-authority'; +import { InstantSearchNext } from 'react-instantsearch-nextjs'; + +import styles from './SearchDialog.module.css'; +import { SearchDialogContent } from './SearchDialogContent/SearchDialogContent'; +import { SearchTrigger } from './SearchTrigger/SearchTrigger'; +import { normalizeMergedSearchHref, normalizeSearchValue } from './utils/searchHitUtils'; + +const algoliaAppId = process.env.NEXT_PUBLIC_ALGOLIA_APP_ID; +const algoliaApiKey = process.env.NEXT_PUBLIC_ALGOLIA_API_KEY; +const algoliaIndexName = process.env.NEXT_PUBLIC_ALGOLIA_INDEX; + +const algoliaDocsIndexName = process.env.NEXT_PUBLIC_ALGOLIA_DOCS_INDEX || 'Docs'; +const DEFAULT_HITS_PER_PAGE = 20; +const DOCS_MAX_HITS_PER_PAGE = 40; +const SEARCH_SOURCE_FIELD = '__searchSource'; +const DOCS_TITLE_SUFFIX_PATTERN = /\s*\|\s*cloudsmith docs\s*$/i; + +// Guard against missing Algolia credentials to prevent breaking the navbar +const hasAlgoliaCredentials = Boolean(algoliaAppId && algoliaApiKey); + +const stripDocsTitleSuffix = (value) => { + if (typeof value === 'string') return value.replace(DOCS_TITLE_SUFFIX_PATTERN, '').trim(); + if (!value || typeof value !== 'object') return value; + + if (Array.isArray(value)) { + return value.map((item) => stripDocsTitleSuffix(item)); + } + + return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, stripDocsTitleSuffix(item)])); +}; + +const normalizeDocsHit = (hit) => ({ + ...hit, + title: stripDocsTitleSuffix(hit?.title), + name: stripDocsTitleSuffix(hit?.name), + hierarchy: stripDocsTitleSuffix(hit?.hierarchy), +}); + +const getRawSearchHitHref = (hit, source) => { + if (source === 'docs') { + return normalizeSearchValue(hit?.url ?? hit?.url_without_anchor); + } + + return normalizeSearchValue(hit?.slug); +}; + +const attachSearchSource = (hit, source) => { + const normalizedHit = source === 'docs' ? normalizeDocsHit(hit) : hit; + const href = getRawSearchHitHref(normalizedHit, source); + + return { + ...normalizedHit, + ...(href ? { slug: normalizeMergedSearchHref(href, source) } : null), + [SEARCH_SOURCE_FIELD]: source, + }; +}; + +const getRankingScore = (hit) => { + const score = hit?._rankingInfo?.userScore ?? hit?._rankingInfo?.score; + return Number.isFinite(score) ? Number(score) : null; +}; + +const annotateHitsForMerge = (hits = [], source) => { + return hits.map((hit, rank) => ({ + hit: attachSearchSource(hit, source), + source, + rank, + })); +}; + +const blendHitsByScore = (websiteHits = [], docsHits = []) => { + const annotatedWebsiteHits = annotateHitsForMerge(websiteHits, 'website'); + const annotatedDocsHits = annotateHitsForMerge(docsHits, 'docs'); + + // Start with docs hits in their original order and only let website hits + // move ahead when they clearly outrank the current docs result. + const result = [...annotatedDocsHits]; + + for (const websiteHit of annotatedWebsiteHits) { + const websiteScore = getRankingScore(websiteHit.hit); + + let insertIndex = result.length; + for (let i = 0; i < result.length; i++) { + const currentScore = getRankingScore(result[i].hit); + + if (websiteScore != null && currentScore != null && websiteScore > currentScore) { + insertIndex = i; + break; + } + } + + result.splice(insertIndex, 0, websiteHit); + } + + return result.map(({ hit }) => hit); +}; + +const mergeMultiIndexResults = (websiteResult, docsResult, request) => { + const requestedHitsPerPage = Number( + request?.params?.hitsPerPage ?? websiteResult?.hitsPerPage ?? DEFAULT_HITS_PER_PAGE, + ); + const effectiveHitsPerPage = Number.isFinite(requestedHitsPerPage) + ? Math.max(1, requestedHitsPerPage) + : DEFAULT_HITS_PER_PAGE; + const websiteHits = websiteResult?.hits || []; + const docsHits = docsResult?.hits || []; + const mergedHits = blendHitsByScore(websiteHits, docsHits).slice(0, effectiveHitsPerPage); + + return { + ...(websiteResult || {}), + hits: mergedHits, + hitsPerPage: effectiveHitsPerPage, + nbHits: (websiteResult?.nbHits || 0) + (docsResult?.nbHits || 0), + nbPages: Math.max(websiteResult?.nbPages || 0, docsResult?.nbPages || 0), + processingTimeMS: (websiteResult?.processingTimeMS || 0) + (docsResult?.processingTimeMS || 0), + query: websiteResult?.query || docsResult?.query || request?.params?.query || '', + params: websiteResult?.params || docsResult?.params || '', + }; +}; + +const getHitsPerPageFromRequest = (request, fallback = DEFAULT_HITS_PER_PAGE) => { + const requestedHitsPerPage = Number(request?.params?.hitsPerPage ?? fallback); + if (!Number.isFinite(requestedHitsPerPage)) return fallback; + + return Math.max(1, requestedHitsPerPage); +}; + +const shouldRequestRankingInfo = (request) => { + const query = normalizeSearchValue(request?.params?.query); + if (!query) return false; + + // Ranking metadata is only used for blending on the first page + const page = Number(request?.params?.page ?? 0); + if (Number.isFinite(page) && page > 0) return false; + + return getHitsPerPageFromRequest(request) > 0; +}; + +// Create a resilient search client that handles connection errors +const createSearchClient = (onError) => { + const buildEmptyResult = (request) => ({ + hits: [], + nbHits: 0, + page: 0, + nbPages: 0, + hitsPerPage: request?.params?.hitsPerPage || DEFAULT_HITS_PER_PAGE, + processingTimeMS: 0, + query: request?.params?.query || '', + params: '', + }); + + const isEmptySearchRequest = (request) => { + return normalizeSearchValue(request?.params?.query).length === 0; + }; + + if (!hasAlgoliaCredentials) { + return { + search: () => Promise.resolve({ results: [] }), + searchForFacetValues: () => Promise.resolve([]), + clearCache: () => {}, + }; + } + + const client = algoliasearch(algoliaAppId, algoliaApiKey); + + // Wrap the search method to handle connection errors gracefully + return { + ...client, + search: async (requests) => { + const normalizedRequests = Array.isArray(requests) ? requests : []; + if (normalizedRequests.length === 0) return { results: [] }; + + // Never send empty/whitespace-only queries to Algolia. + if (normalizedRequests.every(isEmptySearchRequest)) { + onError(null); + + return { + results: normalizedRequests.map((request) => buildEmptyResult(request)), + }; + } + + const websiteRequests = normalizedRequests.map((request) => ({ + ...request, + indexName: request?.indexName || algoliaIndexName, + params: { + ...(request?.params || {}), + ...(shouldRequestRankingInfo(request) ? { getRankingInfo: true } : {}), + }, + })); + + const docsRequests = normalizedRequests.map((request) => { + const includeRankingInfo = shouldRequestRankingInfo(request); + + return { + ...request, + indexName: algoliaDocsIndexName, + params: { + ...(request?.params || {}), + hitsPerPage: Math.min( + getHitsPerPageFromRequest(request, DOCS_MAX_HITS_PER_PAGE), + DOCS_MAX_HITS_PER_PAGE, + ), + ...(includeRankingInfo ? { getRankingInfo: true } : {}), + }, + }; + }); + + try { + const result = await client.search([...websiteRequests, ...docsRequests]); + + const mergedResults = normalizedRequests.map((request, index) => { + const websiteResult = result?.results?.[index]; + const docsResult = result?.results?.[index + normalizedRequests.length]; + + return mergeMultiIndexResults(websiteResult, docsResult, request); + }); + + // Clear any previous errors on successful search + onError(null); + return { results: mergedResults }; + } catch (error) { + // Log the error for debugging + console.warn('Algolia search failed:', error.message); + + // Set error state for UI display + onError('Search is currently unavailable. Please try again later.'); + + // Return empty results that match the expected structure + return { + results: normalizedRequests.map((request) => buildEmptyResult(request)), + }; + } + }, + }; +}; + +export const SearchDialog = () => { + const [open, setOpen] = useState(false); + const [searchError, setSearchError] = useState(null); + const [theme, setTheme] = useState(() => { + if (typeof document === 'undefined') { + return 'undefined'; + } + + return document.body.dataset.theme || 'undefined'; + }); + + // Watch for theme changes on body element + useEffect(() => { + const observer = new MutationObserver(() => { + const newTheme = document.body.dataset.theme || 'undefined'; + setTheme(newTheme); + }); + + observer.observe(document.body, { + attributes: true, + attributeFilter: ['data-theme'], + }); + + return () => observer.disconnect(); + }, []); + + const overlayClasses = cx(styles.overlay, { + [styles.themeLight]: theme === 'light' || theme === 'undefined', + [styles.themeDark]: theme === 'dark', + }); + + const contentClasses = cx(styles.content, { + [styles.themeLight]: theme === 'light' || theme === 'undefined', + [styles.themeDark]: theme === 'dark', + }); + + // Create search client with error handler + const searchClient = useMemo(() => createSearchClient(setSearchError), []); + if (!hasAlgoliaCredentials) return null; + + return ( + + + + + + + Search + + + + setOpen(false)} searchError={searchError} /> + + + + + + ); +}; diff --git a/src/components/SearchDialog/SearchDialog.module.css b/src/components/SearchDialog/SearchDialog.module.css index 0d31fb87..974fd5f9 100644 --- a/src/components/SearchDialog/SearchDialog.module.css +++ b/src/components/SearchDialog/SearchDialog.module.css @@ -1,183 +1,84 @@ +/* Dialog shell */ .overlay { - display: grid; position: fixed; z-index: var(--layer-overlay); inset: 0; - overflow-y: auto; - animation: overlayShow 150ms ease-in-out; + padding: 42px 16px 57px; + display: grid; + place-items: center; + overflow-y: hidden; background-color: var(--color-overlay-dark); - place-items: start center; + + @media (--laptop-up) { + padding: 0; + place-items: start center; + } } .content { + --search-icon-size: 20px; --padding-block: var(--space-m); --padding-inline: var(--space-l); + --dialog-content-height: 625px; + --dialog-header-height: 48px; - background-color: var(--base-color-white); position: relative; + display: flex; + flex-direction: column; width: 100%; max-width: 1040px; + height: var(--dialog-content-height); overflow: hidden; - animation: contentShow 250ms ease-in-out; + background-color: var(--base-color-white); border: 1px solid var(--color-border); border-radius: var(--border-radius-l); box-shadow: 0 0 2px 1px var(--color-dialog-box-shadow); } -.header { - display: flex; - position: relative; - flex-wrap: wrap; - padding: 0 var(--space-xs); - border-bottom: 1px solid var(--color-border); - background-color: var(--color-background-header); -} - -.iconWrapper { - flex: 0 0 var(--padding-inline); - display: flex; - justify-content: center; - align-items: center; - color: var(--color-text-secondary); -} - -.icon { - transform: translateY(-1px); -} - -.main { - padding-block: var(--space-s); - padding-inline: var(--space-s); - overflow-y: auto; - max-height: calc(100vh - 50px); -} - -.results { - list-style: none; - padding: 0; - margin: 0; - display: flex; - flex-direction: column; - row-gap: var(--space-xs); -} - -.resultLink { - display: grid; - grid-template-areas: - 'title enter arrow' - 'description enter arrow'; - grid-template-columns: 1fr auto auto; - column-gap: var(--space-xs); - padding-block: var(--space-xs); - padding-inline: 20px; - border-radius: var(--border-radius-s); - text-decoration: none; - color: var(--color-text-secondary); - - &:is(:hover, :focus, .resultLinkFocus) { - outline: none; - background-color: var(--brand-color-blue-1); - - .resultTag { - outline: 1px solid var(--base-color-blue-75); - } - } -} - -.resultTitle { - grid-area: title; - color: var(--brand-color-grey-10); - display: flex; - align-items: center; - gap: var(--space-xs); -} +/* Light theme */ +.content.themeLight { + --color-text-default: var(--brand-color-grey-10); + --color-text-secondary: var(--brand-color-grey-8); + --color-background-default: var(--brand-color-grey-1); + --color-background-hover: var(--brand-color-grey-2); + --color-border-light: var(--brand-color-grey-1); + --color-border: var(--brand-color-grey-2); + --color-reset-button-hover: var(--brand-color-grey-10); -.resultDescription { - grid-area: description; - color: var(--brand-color-grey-8); - opacity: 0.65; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; -} - -.resultArrow { - grid-area: arrow; - align-self: center; - color: var(--brand-color-grey-8); - width: 16px; - flex-shrink: 0; + background-color: var(--base-color-white); + color: var(--color-text-default); + border-color: var(--color-border); } -.noResults { - color: var(--brand-color-grey-7); - text-align: center; - padding: var(--space-m); - margin: 0; +.triggerLight { + --search-trigger-background: var(--color-background-light); + --search-trigger-background-hover: var(--color-border); + --search-trigger-icon-color: var(--brand-color-grey-8); } -.footer { - background-color: var(--base-color-grey-50); - padding-block: var(--space-3xs); - padding-inline: var(--padding-inline); - border-top: 1px solid var(--color-border); -} +/* Dark theme */ +.content.themeDark { + --color-text-default: var(--base-color-white); + --color-text-secondary: var(--brand-color-grey-4); + --color-background-default: var(--brand-color-grey-9); + --color-background-hover: var(--brand-color-grey-8); + --color-border: var(--brand-color-grey-8); + --color-reset-button-hover: var(--brand-color-grey-3); -.data { - display: flex; - align-items: center; - justify-content: center; - gap: var(--space-xs); - background-color: var(--base-color-blue-100); - border-radius: var(--border-radius-s); - padding-inline: var(--space-3xs); - padding-block: var(--space-4xs); - color: var(--base-color-grey-600); - min-inline-size: 30px; - align-self: center; - height: fit-content; + background-color: var(--brand-color-grey-10); + color: var(--color-text-default); + border-color: var(--color-border); } -@media (--phablet-down) { - .footer { - display: none; - } +.triggerDark { + --search-trigger-background: var(--brand-color-grey-8); + --search-trigger-background-hover: var(--brand-color-grey-7); + --search-trigger-icon-color: var(--brand-color-white); } -@media (--phablet-up) { +/* Media queries */ +@media (--tablet-up) { .content { - margin-block: var(--space-l); - } - - .header { - padding-bottom: 0; - } - - .main { - max-height: calc(100vh - 150px); - } -} - -@keyframes overlayShow { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - -@keyframes contentShow { - from { - transform: translate(0, -3%); - opacity: 0; - } - - to { - transform: translate(0, 0); - opacity: 1; + margin-block: var(--space-4xl); } } diff --git a/src/components/SearchDialog/SearchDialog.tsx b/src/components/SearchDialog/SearchDialog.tsx deleted file mode 100644 index 6010eb7a..00000000 --- a/src/components/SearchDialog/SearchDialog.tsx +++ /dev/null @@ -1,227 +0,0 @@ -'use client'; - -import { Dispatch, SetStateAction, useEffect, useState } from 'react'; - -import * as RadixDialog from '@radix-ui/react-dialog'; -import { VisuallyHidden } from '@radix-ui/react-visually-hidden'; -import { cx } from 'class-variance-authority'; -import Link from 'next/link'; -import { useRouter } from 'next/navigation'; -import { useHotkeys } from 'react-hotkeys-hook'; - -import { Icon, IconName } from '@/icons'; -import { useShowKeyboardHints } from '@/lib/hooks'; -import { performSearch } from '@/lib/search/server'; -import { SearchResult } from '@/lib/search/types'; -import { debounce } from '@/lib/util'; - -import { Tag } from '../Tag'; -import { FilterButtons } from './FilterButtons'; -import styles from './SearchDialog.module.css'; -import { SearchForm } from './SearchForm'; -import { SearchTrigger } from './SearchTrigger'; - -export const filters: Filters = [ - { id: 'documentation', label: 'Documentation', icon: 'action/documentation' }, - { id: 'guides', label: 'Guides', icon: 'utility/guide' }, - { id: 'api', label: 'API Reference', icon: 'action/api' }, -]; - -/** - * If the requirements for this become even more sophisticated, - * or we need to implement client-side fetched anywhere else, - * we should replace this with useSWR. For now, it's simple and works. - */ -const debouncedSearch = debounce( - async ( - term: string, - sections: string[], - setIsWaiting: Dispatch>, - setResults: Dispatch>, - setFocusedIndex: Dispatch>, - ) => { - const results = await performSearch(term, sections); - setIsWaiting(false); - setResults(results); - setFocusedIndex(0); - }, - 300, -); - -export const SearchDialog = () => { - const [open, setOpen] = useState(false); - const [term, setTerm] = useState(''); - const [isWaiting, setIsWaiting] = useState(false); - const [sections, setSections] = useState>([filters[0].id]); - const [results, setResults] = useState([]); - const [focusedIndex, setFocusedIndex] = useState(0); - const [isKeyboardNav, setIsKeyboardNav] = useState(false); - const router = useRouter(); - const showKeyboardHints = useShowKeyboardHints(); - - useHotkeys(['mod+k'], () => setOpen(true)); - - // Reset keyboard nav when results change - useEffect(() => { - setIsKeyboardNav(false); - }, [results]); - - useEffect(() => { - if (term === '') { - setResults([]); - } else { - setIsWaiting(true); - debouncedSearch(term, sections, setIsWaiting, setResults, setFocusedIndex); - } - }, [term, sections, setResults]); - - const setSection = (section: Section) => { - setSections((prev) => { - if (prev.includes(section)) { - // Don't remove if it would leave no sections - if (prev.length === 1) { - return prev; - } - - return prev.filter((s) => s !== section); - } - - return [...prev, section]; - }); - }; - - const scrollIntoViewIfNeeded = (element: HTMLElement | null) => { - if (isKeyboardNav && element) { - element.scrollIntoView({ block: 'nearest' }); - } - }; - - const goUp = () => { - setIsKeyboardNav(true); - setFocusedIndex((prev) => { - if (prev === 0) { - return results.length - 1; - } - return prev - 1; - }); - }; - - const goDown = () => { - setIsKeyboardNav(true); - setFocusedIndex((prev) => { - if (prev === results.length - 1) { - return 0; - } - return prev + 1; - }); - }; - - const goToResult = () => { - if (focusedIndex !== -1) { - router.push(results[focusedIndex].path); - setOpen(false); - } - }; - - const goToStart = () => { - setIsKeyboardNav(true); - setFocusedIndex(0); - }; - - const goToEnd = () => { - setIsKeyboardNav(true); - setFocusedIndex(results.length - 1); - }; - - return ( - - - - - - -
-
- -
- - {showKeyboardHints && ( -
- ESC -
- )} -
- - - - - - Search - - Search for documentation, guides, and API reference. - - - -
- - -
    { - if (isKeyboardNav) { - setIsKeyboardNav(false); - } - }}> - {results.map((res, index) => ( -
  • - !isKeyboardNav && setFocusedIndex(index)} - onClick={() => setOpen(false)} - className={cx(styles.resultLink, { [styles.resultLinkFocus]: index === focusedIndex })}> - - {res.title} - {res.method && } - - - {highlightSearchTerm(res.snippet, term)} - - - -
  • - ))} -
- - {isWaiting &&

Loading...

} - - {!isWaiting && term === '' && ( -

Please enter a search term

- )} - - {!isWaiting && results.length === 0 && term !== '' && ( -

No results

- )} -
-
-
-
-
- ); -}; - -const highlightSearchTerm = (text: string, term: string) => { - if (!term.trim()) { - return text; - } - - return text - .split(new RegExp(`(${term})`, 'gi')) - .map((part, i) => (part.toLowerCase() === term.toLowerCase() ? {part} : part)); -}; - -export type Filters = Array<{ id: string; label: string; icon: IconName }>; -type Section = (typeof filters)[number]['id']; diff --git a/src/components/SearchDialog/SearchDialogContent/SearchDialogContent.jsx b/src/components/SearchDialog/SearchDialogContent/SearchDialogContent.jsx new file mode 100644 index 00000000..79db44db --- /dev/null +++ b/src/components/SearchDialog/SearchDialogContent/SearchDialogContent.jsx @@ -0,0 +1,90 @@ +'use client'; + +import { useCallback, useEffect, useRef } from 'react'; + +import { Configure, SearchBox, useInstantSearch, useSearchBox } from 'react-instantsearch'; + +import { Icon } from '@/icons'; + +import styles from './SearchDialogContent.module.css'; +import { SearchFilters } from './SearchFilters/SearchFilters'; +import { SearchResults } from './SearchResults/SearchResults'; + +export const SearchDialogContent = ({ onClose, searchError }) => { + const { results, status } = useInstantSearch(); + const { query } = useSearchBox(); + const debounceRef = useRef(null); + const trimmedQuery = query?.trim() || ''; + const displayedHits = results?.hits || []; + const settledQuery = results?.query?.trim?.() || ''; + const showReset = trimmedQuery.length > 0; + const isSearching = status === 'loading' || status === 'stalled'; + const showResultsLayout = trimmedQuery.length > 0; + + const queryHook = useCallback((nextQuery, search) => { + const trimmed = nextQuery.trim(); + + if (trimmed.length === 0) { + if (debounceRef.current) window.clearTimeout(debounceRef.current); + search(''); + return; + } + + if (trimmed.length < 2) return; + if (debounceRef.current) window.clearTimeout(debounceRef.current); + + debounceRef.current = window.setTimeout(() => { + search(nextQuery); + }, 150); + }, []); + + useEffect( + () => () => { + if (debounceRef.current) window.clearTimeout(debounceRef.current); + }, + [], + ); + + return ( + <> +
+
+ +
+ } + loadingIconComponent={() => null} + autoFocus + classNames={{ + root: styles.searchBox, + input: styles.searchBoxInput, + form: styles.searchBoxForm, + submit: styles.searchBoxSubmit, + reset: `${styles.searchBoxReset} ${ + showReset ? styles.searchBoxResetVisible : styles.searchBoxResetHidden + }`, + }} + /> + + esc + +
+ + +
+ + +
+ + ); +}; diff --git a/src/components/SearchDialog/SearchDialogContent/SearchDialogContent.module.css b/src/components/SearchDialog/SearchDialogContent/SearchDialogContent.module.css new file mode 100644 index 00000000..9a323a77 --- /dev/null +++ b/src/components/SearchDialog/SearchDialogContent/SearchDialogContent.module.css @@ -0,0 +1,137 @@ +/* Header */ +.header { + display: flex; + align-items: stretch; /* add this to ensure full height */ + padding: 0 var(--space-xs); + gap: var(--space-2xs); + border-bottom: 1px solid var(--color-border); + background-color: var(--color-background-header); +} + +.iconWrapper { + display: flex; + flex: 0; + align-items: center; + justify-content: center; + color: var(--color-text-secondary); +} + +.icon { + transform: translateY(-1px); + color: inherit; + + & path { + fill: currentcolor; + } +} + +.searchBox { + display: flex; + flex: 1; +} + +.searchBoxForm { + display: flex; + align-items: center; + width: 100%; +} + +.searchBoxInput { + flex: 1; + width: 100%; + padding: var(--space-xs) 0; + background-color: transparent; + border: 1px solid transparent; + border-radius: 0; + outline: none; + font-family: var(--font-family-body); + font-size: var(--text-body-m); + text-overflow: ellipsis; + color: var(--color-text-secondary); + + @media (--laptop-up) { + font-size: var(--text-body-s); + } + + &::placeholder { + color: var(--color-text-tertiary); + } + + &[type='search'], + &::-webkit-search-cancel-button, + &::-webkit-search-decoration { + appearance: none; + } +} + +.searchBoxSubmit { + display: none; +} + +.searchBoxReset { + display: flex; + align-items: center; + justify-content: center; + margin-left: var(--space-3xs); + padding: var(--space-2xs); + background: none; + border: 0; + color: var(--color-text-secondary); + cursor: pointer; + transition: var(--transition-fast); + + &:hover { + color: var(--color-reset-button-hover); + } +} + +.resetIcon { + width: 14px; + height: 14px; +} + +.searchBoxResetHidden { + opacity: 0; + pointer-events: none; +} + +.searchBoxResetVisible { + opacity: 1; + pointer-events: auto; + outline: none; +} + +.kbdBadge { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-xs); + margin-left: auto; + height: fit-content; + padding-block: var(--space-4xs); + padding-inline: var(--space-3xs); + background-color: var(--color-background-default); + border-radius: var(--border-radius-s); + align-self: center; + color: var(--color-text-secondary); + font-size: var(--text-body-2xs); + text-transform: uppercase; +} + +/* Results layout */ +.resultsWrapper { + display: grid; + grid-template-columns: minmax(0, 1fr); + overflow-y: auto; + height: 100%; + scrollbar-color: var(--color-background-default) transparent; + scrollbar-width: thin; + + &::-webkit-scrollbar-thumb { + background: var(--color-background-default); + } + + @media (--tablet-up) { + grid-template-columns: 225px minmax(0, 1fr); + } +} diff --git a/src/components/SearchDialog/SearchDialogContent/SearchFilters/SearchFilters.jsx b/src/components/SearchDialog/SearchDialogContent/SearchFilters/SearchFilters.jsx new file mode 100644 index 00000000..cd45acfe --- /dev/null +++ b/src/components/SearchDialog/SearchDialogContent/SearchFilters/SearchFilters.jsx @@ -0,0 +1,69 @@ +import { useMemo } from 'react'; + +import { Icon } from '@/icons'; + +import { filtersData } from '../../utils/data'; +import { + buildSearchGroups, + RECOMMENDED_GROUP_TYPE, + sortSearchGroupsForFilters, +} from '../../utils/searchGroupUtils'; +import styles from './SearchFilters.module.css'; + +export const SearchFilters = ({ hits, query }) => { + const groupedHits = useMemo(() => { + return sortSearchGroupsForFilters(buildSearchGroups(hits || [])); + }, [hits]); + + if (!query || groupedHits.length === 0) return null; + + const scrollToGroup = (groupId) => { + const target = document.getElementById(groupId); + if (!target) return; + + // Find the scrollable container (SearchResults root) + const scrollContainer = target.closest('[class*="root"]'); + if (!scrollContainer) return; + + // Calculate position relative to the scroll container + const containerRect = scrollContainer.getBoundingClientRect(); + const targetRect = target.getBoundingClientRect(); + const targetRelativeTop = targetRect.top - containerRect.top; + + scrollContainer.scrollTo({ + top: scrollContainer.scrollTop + targetRelativeTop - 16, + behavior: 'smooth', + }); + }; + + return ( + + ); +}; diff --git a/src/components/SearchDialog/SearchDialogContent/SearchFilters/SearchFilters.module.css b/src/components/SearchDialog/SearchDialogContent/SearchFilters/SearchFilters.module.css new file mode 100644 index 00000000..96899d97 --- /dev/null +++ b/src/components/SearchDialog/SearchDialogContent/SearchFilters/SearchFilters.module.css @@ -0,0 +1,68 @@ +.groups { + display: none; + position: sticky; + top: 0; + align-self: start; + width: 100%; + min-width: 0; + overflow-y: auto; + height: 100%; + padding: var(--space-s) var(--space-s) 0 0; + border-right: 1px solid var(--color-border); + + @media (--tablet-up) { + display: block; + } +} + +.groupList { + flex-direction: column; + list-style: none; + margin: 0; + padding: 0; + display: flex; +} + +.groupItem { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-2xs); + width: 100%; + padding: var(--space-2xs) var(--space-xs); + background: transparent; + border: 0; + border-radius: var(--border-radius-s); + font-family: var(--font-family-body); + font-size: var(--text-body-xs); + text-align: left; + color: var(--color-text-secondary); + cursor: pointer; + + &:hover, + &:focus-visible { + color: var(--color-text-default); + outline: none; + } +} + +.groupItemLabel { + display: flex; + gap: var(--space-2xs); + align-items: center; + font-size: var(--text-body-s); +} + +.groupItemIcon { + width: var(--search-icon-size); + height: var(--search-icon-size); + color: inherit; + + & path { + fill: currentcolor; + } +} + +.groupItemCount { + color: inherit; +} diff --git a/src/components/SearchDialog/SearchDialogContent/SearchFilters/index.js b/src/components/SearchDialog/SearchDialogContent/SearchFilters/index.js new file mode 100644 index 00000000..4f778a58 --- /dev/null +++ b/src/components/SearchDialog/SearchDialogContent/SearchFilters/index.js @@ -0,0 +1,3 @@ +import { SearchFilters } from './SearchFilters'; + +export { SearchFilters }; diff --git a/src/components/SearchDialog/SearchDialogContent/SearchResults/SearchResults.jsx b/src/components/SearchDialog/SearchDialogContent/SearchResults/SearchResults.jsx new file mode 100644 index 00000000..e42e4d55 --- /dev/null +++ b/src/components/SearchDialog/SearchDialogContent/SearchResults/SearchResults.jsx @@ -0,0 +1,295 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; + +import { cx } from 'class-variance-authority'; +import { useRouter } from 'next/navigation'; + +import { Link } from '@/components/Link'; +import { Icon } from '@/icons'; +import { isExternalHref } from '@/util/url'; + +import { filtersData } from '../../utils/data'; +import { + buildSearchGroups, + formatGroupLabel, + getSearchGroupType, + RECOMMENDED_GROUP_TYPE, +} from '../../utils/searchGroupUtils'; +import { getHitHref, normalizeSearchValue, shouldOpenSearchHitInNewTab } from '../../utils/searchHitUtils'; +import styles from './SearchResults.module.css'; + +const getHitTitle = (hit) => { + return ( + normalizeSearchValue(hit?.title) || + normalizeSearchValue(hit?.name) || + normalizeSearchValue(hit?.hierarchy?.lvl2) || + normalizeSearchValue(hit?.hierarchy?.lvl1) || + normalizeSearchValue(hit?.hierarchy?.lvl0) + ); +}; + +const getHitCategory = (hit) => { + return ( + normalizeSearchValue(hit?.category) || + normalizeSearchValue(hit?.section) || + normalizeSearchValue(hit?.hierarchy?.lvl0) + ); +}; + +const getHitObjectId = (hit, fallbackIndex) => { + return ( + normalizeSearchValue(hit?.objectID) || + `${normalizeSearchValue(hit?._type || hit?.type || 'result')}-${getHitHref(hit) || getHitTitle(hit) || fallbackIndex}` + ); +}; + +const navigateToHit = (slug, router, opensInNewTab) => { + if (!slug) return; + + if (opensInNewTab) { + window.open(slug, '_blank', 'noopener,noreferrer'); + return; + } + + if (isExternalHref(slug)) { + window.location.assign(slug); + return; + } + + router.push(slug); +}; + +const Hit = ({ hit, group, groupType, onClose }) => { + const href = getHitHref(hit); + const title = getHitTitle(hit); + const opensInNewTab = shouldOpenSearchHitInNewTab(hit); + const hitType = getSearchGroupType(hit); + const hitFilter = filtersData.find((filter) => filter.documentType === hitType); + const resultIcon = groupType === RECOMMENDED_GROUP_TYPE ? hitFilter?.icon : group?.icon; + const resultGroupLabel = formatGroupLabel(groupType === RECOMMENDED_GROUP_TYPE ? hitType : groupType); + const resultCategory = getHitCategory(hit); + const shouldShowCategory = + resultCategory.length > 0 && resultCategory.toLowerCase() !== resultGroupLabel.toLowerCase(); + + const hitContent = ( + <> + {resultIcon && ( +
+ +
+ )} +
+ {title} + + {resultGroupLabel} + {shouldShowCategory && ( + <> + + {resultCategory} + + )} + +
+