diff --git a/cypress/integration/site-search/index.spec.ts b/cypress/integration/site-search/index.spec.ts index 9107fa017c..e3f08791a4 100644 --- a/cypress/integration/site-search/index.spec.ts +++ b/cypress/integration/site-search/index.spec.ts @@ -5,7 +5,7 @@ describe("Docs website search", () => { }); beforeEach(() => { - cy.intercept({ url: "https://**.algolia.net/**", method: "POST" }).as("searchRequest"); + cy.intercept({ url: "/api/docs-search", method: "POST" }).as("searchRequest"); }); beforeEach(() => { @@ -14,10 +14,14 @@ describe("Docs website search", () => { it("should handle a search string", () => { cy.get("@searchButtonEl").scrollIntoView().should("be.visible").click({ force: true }); - cy.get(".DocSearch-Input").should("be.visible").should("be.focused").type("checkbox"); + cy.get('[data-cy="paste-docsearch-input"]') + .should("be.visible") + .should("be.focused") + .type("checkbox") + .type("{enter}"); cy.wait("@searchRequest"); - cy.get(".DocSearch-Hits").should("have.length.above", 0); - cy.get('.DocSearch-Hits [role="listbox"]').should("have.length.above", 0); - cy.get('.DocSearch-Hits [role="option"]').should("have.length.above", 0); + cy.get('[data-cy="paste-docsearch-hits"] h2').should("have.length.above", 0); + cy.get('[data-cy="paste-docsearch-hits"] ul').should("have.length.above", 0); + cy.get('[data-cy="paste-docsearch-hits"] li').should("have.length.above", 0); }); }); diff --git a/internal-docs/engineering/developing-locally.md b/internal-docs/engineering/developing-locally.md index 55e73ae2ea..8a0b79973d 100644 --- a/internal-docs/engineering/developing-locally.md +++ b/internal-docs/engineering/developing-locally.md @@ -180,24 +180,6 @@ DATADOG_CLIENT_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxx ID of the Airtable that contains our roadmap - - NEXT_PUBLIC_DOCSEARCHV3_APIKEY - - API key for powering our docs search - - - - NEXT_PUBLIC_DOCSEARCHV3_APPID - - Docs search app ID - - - - NEXT_PUBLIC_DOCSEARCHV3_INDEXNAME - - twilio_paste - - ## Building packages diff --git a/internal-docs/engineering/doc-site/docsearch.md b/internal-docs/engineering/doc-site/docsearch.md index 0a06a4631d..9992eecb1f 100644 --- a/internal-docs/engineering/doc-site/docsearch.md +++ b/internal-docs/engineering/doc-site/docsearch.md @@ -1,17 +1,23 @@ # Docsearch -The search is provided by [Algolia Docsearch v3](https://docsearch.algolia.com/docs/DocSearch-v3/). +The search is provided a custom index hosted on Supabase and created by us on a [nightly Cron Github Action](https://github.com/twilio-labs/paste/blob/main/.github/workflows/update_docs_embed.yml). -The docsearch app login is stored in OnePassword, where you can [login here](https://www.algolia.com/). +The Supabase login is stored in OnePassword. -The dashboard will give you analytics and some configurations and insights into the index that is created. +## Creating Embeddings -## Crawler +We use the same technology that powers our GPT4 powered Discussions and Chat bot, to create our Site Search. -Docsearch crawls the website automatically via the crawler. It is recrawled every day. To configure the website crawler you must log into the [crawler application](https://crawler.algolia.com/). +On a nightly basis we index new or updated content in our MDX files and Github Discussions. -The crawler is configured via the editor, and a configuration object. It controls where the sitemap live, how often to crawl, and how to extract data from the website pages into the search index. +For MDX files we create "sections" of "pages" by chunking mdx files by headings. Those sections are then converted to OPENAI Vector embeds and stored in the Supabase Vector DB. -Use the Crawler app to visualize what Algolia can extract from our web pages as it crawls, and test any configuration changes you make. +The same happens with Github discussions. The discussion is a page and is split into two sections, the initial post and the answer. -Crawler documentation can be found [here](https://www.algolia.com/doc/tools/crawler/apis/configuration/). +We can then perform a similarity search using the user input as the query. + +Embeddings are generated using the the [embedding script](../../../packages/paste-website/scripts/search/). + +## Returning results + +We have a [NextJS API route](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) `/api/documentation-search`. It will return the top 10 most similar page sections across MDX and Github discussions based on a user `prompt`. diff --git a/internal-docs/engineering/environment-variables.md b/internal-docs/engineering/environment-variables.md index faee8a89bb..6387aee33c 100644 --- a/internal-docs/engineering/environment-variables.md +++ b/internal-docs/engineering/environment-variables.md @@ -12,9 +12,6 @@ Full list of Environment variables and where they are needed. | DATADOG_CLIENT_TOKEN | Datadog client token for remix RUM tracking | ✅ | | | ✅ | | NEXT_PUBLIC_DATADOG_APPLICATION_ID | Datadog application ID for docsite RUM tracking | ✅ | | ✅ | | | NEXT_PUBLIC_DATADOG_CLIENT_TOKEN | Datadog client token for docsite RUM tracking | ✅ | | ✅ | | -| NEXT_PUBLIC_DOCSEARCHV3_APIKEY | Algolia API key | ✅ | | ✅ | | -| NEXT_PUBLIC_DOCSEARCHV3_APPID | Algolia app ID | ✅ | | ✅ | | -| NEXT_PUBLIC_DOCSEARCHV3_INDEXNAME | Algolia Index name | ✅ | | ✅ | | | NEXT_PUBLIC_ENVIRONMENT_CONTEXT | Informs Next which deployment environment its in | ✅ | | ✅ | | | NETLIFY_SITE_ID | Docsite site ID for Netlify deployment wait in GH actions | | ✅ | | | | NETLIFY_TOKEN | Docsite token for Netlify deployment wait in GH actions | | ✅ | | | diff --git a/internal-docs/engineering/technologies.md b/internal-docs/engineering/technologies.md index 7ae5600799..034633755c 100644 --- a/internal-docs/engineering/technologies.md +++ b/internal-docs/engineering/technologies.md @@ -96,7 +96,7 @@ The documentation site is a [NextJS](https://nextjs.org/) website using the late The content is largely written in [MDX](https://mdxjs.com/) pages. -The search is provided by [Algolia Docsearch](https://docsearch.algolia.com/). +The search is custom built based on OpenAI embeddings, a vector DB and a similarity search that allows us to return results across mdx pages and Github discussions. [Learn more](https://github.com/twilio-labs/paste/blob/main/internal-docs/engineering/doc-site/docsearch.md). The [roadmap](https://paste.twilio.design/roadmap/) is stored in an [Airtable](https://airtable.com/) and imported upon building the website via the [Airtable JS client](https://github.com/Airtable/airtable.js). diff --git a/packages/paste-website/package.json b/packages/paste-website/package.json index b09333d31d..186c9007e7 100644 --- a/packages/paste-website/package.json +++ b/packages/paste-website/package.json @@ -20,8 +20,6 @@ }, "dependencies": { "@datadog/browser-rum": "^4.46.0", - "@docsearch/css": "^3.3.3", - "@docsearch/react": "^3.3.3", "@hookform/error-message": "2.0.0", "@mdx-js/loader": "^1.6.22", "@mdx-js/mdx": "^1.6.22", @@ -170,6 +168,7 @@ "react-dom": "^18.0.0", "react-github-button": "^0.1.11", "react-hook-form": "^7.30.0", + "react-hotkeys-hook": "^4.4.1", "react-live": "^3.1.1", "react-scrollspy": "^3.4.0", "react-visibility-sensor": "5.1.1", @@ -186,6 +185,7 @@ }, "devDependencies": { "@next/eslint-plugin-next": "^13.1.6", + "@storybook/react": "7.0.6", "@testing-library/react": "^13.4.0", "tsx": "^3.12.10" }, diff --git a/packages/paste-website/src/components/ContactUsMenu.tsx b/packages/paste-website/src/components/ContactUsMenu.tsx index 0003124285..5b343a6058 100644 --- a/packages/paste-website/src/components/ContactUsMenu.tsx +++ b/packages/paste-website/src/components/ContactUsMenu.tsx @@ -20,7 +20,9 @@ export const ContactUsMenu: React.FC = () => { <> event({ @@ -30,7 +32,7 @@ export const ContactUsMenu: React.FC = () => { }) } > - + { + const maskID = useUID(); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/paste-website/src/components/site-search/SearchEmptyState.tsx b/packages/paste-website/src/components/site-search/SearchEmptyState.tsx new file mode 100644 index 0000000000..75715ddb93 --- /dev/null +++ b/packages/paste-website/src/components/site-search/SearchEmptyState.tsx @@ -0,0 +1,21 @@ +import { Box } from "@twilio-paste/box"; +import { Heading } from "@twilio-paste/heading"; +import { Paragraph } from "@twilio-paste/paragraph"; +import * as React from "react"; + +import { SearchEmptyIllustration } from "./SearchEmptyIllustration"; + +export const SearchEmptyState: React.FC<{ searchQuery: string }> = ({ searchQuery }) => { + return ( + + + ); +}; diff --git a/packages/paste-website/src/components/site-search/SearchForm.tsx b/packages/paste-website/src/components/site-search/SearchForm.tsx new file mode 100644 index 0000000000..f68dfd39f4 --- /dev/null +++ b/packages/paste-website/src/components/site-search/SearchForm.tsx @@ -0,0 +1,75 @@ +import { Box } from "@twilio-paste/box"; +import { Button } from "@twilio-paste/button"; +import { Form, FormControl, FormSection } from "@twilio-paste/form"; +import { ClearIcon } from "@twilio-paste/icons/esm/ClearIcon"; +import { SearchIcon } from "@twilio-paste/icons/esm/SearchIcon"; +import { Input } from "@twilio-paste/input"; +import { useUID } from "@twilio-paste/uid-library"; +import * as React from "react"; + +export interface SearchFormProps { + onSubmit: () => void; + onClear: () => void; + hasResults: boolean; + ariaLabelledBy: string; + inputValue: string; + onChange: (event: React.ChangeEvent) => void; + inputRef: React.RefObject; +} + +const SearchForm: React.FC> = ({ + ariaLabelledBy, + onClear, + onSubmit, + hasResults, + inputValue, + onChange, + inputRef, +}) => { + const inputID = useUID(); + return ( + +
{ + e.preventDefault(); + onSubmit(); + }} + > + + + } + insertAfter={ + hasResults && ( + + ) + } + aria-labelledby={ariaLabelledBy} + type="text" + id={inputID} + name="search-input" + placeholder={'Try "button" or "what is a design token?"'} + value={inputValue} + onChange={onChange} + data-cy="paste-docsearch-input" + ref={inputRef} + /> + + +
+
+ ); +}; + +SearchForm.displayName = "SearchForm"; + +export { SearchForm }; diff --git a/packages/paste-website/src/components/site-search/SearchModal.tsx b/packages/paste-website/src/components/site-search/SearchModal.tsx new file mode 100644 index 0000000000..8e004b4ba6 --- /dev/null +++ b/packages/paste-website/src/components/site-search/SearchModal.tsx @@ -0,0 +1,33 @@ +import { Box } from "@twilio-paste/box"; +import { Modal, ModalBody, ModalHeader, ModalHeading, type ModalProps } from "@twilio-paste/modal"; +import * as React from "react"; + +type SearchModal = Pick & { + children: NonNullable; +}; +export const SearchModal: React.FC> = ({ + isOpen, + ariaLabelledby, + onDismiss, + initialFocusRef, + children, +}) => { + return ( + + + + + Search documentation and discussions + + + + {children} + + ); +}; diff --git a/packages/paste-website/src/components/site-search/SearchResultDocs.tsx b/packages/paste-website/src/components/site-search/SearchResultDocs.tsx new file mode 100644 index 0000000000..e22771f1a6 --- /dev/null +++ b/packages/paste-website/src/components/site-search/SearchResultDocs.tsx @@ -0,0 +1,102 @@ +import { Anchor } from "@twilio-paste/anchor"; +import { Box } from "@twilio-paste/box"; +import { InlineCode } from "@twilio-paste/inline-code"; +import Markdown from "markdown-to-jsx"; +import Link from "next/link"; +import * as React from "react"; + +import { type DocSearchItem } from "./types"; + +export const SearchResultHeading: React.FC = ({ children }) => { + return ( + + {children} + + ); +}; + +export const SearchResultParagraph: React.FC = ({ children }) => { + return ( + + {children} + + ); +}; + +export const SearchResultList: React.FC = ({ children }) => { + return ( + + {children} + + ); +}; + +export const SearchResultListItem: React.FC = ({ children }) => { + return ( + + {children} + + ); +}; + +export const SearchResultDocs: React.FC<{ searchItem: DocSearchItem }> = ({ searchItem }) => { + return ( + + + {/* trim the content string to just 140 characters */} + {`${searchItem.content.slice(0, 120)}...`} + + + ); +}; diff --git a/packages/paste-website/src/components/site-search/SearchResults.tsx b/packages/paste-website/src/components/site-search/SearchResults.tsx new file mode 100644 index 0000000000..f4d0c6152b --- /dev/null +++ b/packages/paste-website/src/components/site-search/SearchResults.tsx @@ -0,0 +1,163 @@ +import { secureExternalLink } from "@twilio-paste/anchor"; +import { Box } from "@twilio-paste/box"; +import { Heading } from "@twilio-paste/heading"; +import { DocumentationIcon } from "@twilio-paste/icons/esm/DocumentationIcon"; +import { LinkExternalIcon } from "@twilio-paste/icons/esm/LinkExternalIcon"; +import Link from "next/link"; +import * as React from "react"; + +import { sentenceCase } from "../../utils/SentenceCase"; +import GithubIcon from "../icons/GithubIcon"; +import { SearchResultDocs } from "./SearchResultDocs"; +import { type DocSearchItem, type GroupedSearchResults } from "./types"; + +export interface SearchResultsProps { + results?: GroupedSearchResults; +} + +const DiscussionHeading: React.FC<{ title: string; path: string }> = ({ title, path }) => { + return ( + + + + + + + {title} + + + + + ); +}; + +const DocumentationHeading: React.FC<{ title: string; slug: string }> = ({ title, slug }) => { + return ( + + + + + + {sentenceCase(slug.split("/")[1])} + + + {title} + + + + + ); +}; + +const SearchResultsList: React.FC = ({ results }) => { + if (!results) { + return null; + } + return ( + <> + {Object.keys(results).map((path) => { + const resultSections = results[path]; + const resultType = resultSections[0].type; + const resultParent = { + path: resultSections[0].path, + title: resultSections[0].meta.title, + ...(resultType === "markdown" && { + description: resultSections[0].meta.description, + slug: resultSections[0].meta.slug, + }), + }; + return ( + + {resultType === "github-discussions" && ( + + )} + {resultType === "markdown" && resultParent.slug != null && ( + + )} + {resultParent.description && ( + + {resultParent.description} + + )} + {resultType === "markdown" && ( + + + {resultSections.map((result) => { + return ( + + + + ); + })} + + + )} + + ); + })} + + ); +}; + +SearchResultsList.displayName = "SearchResultsList"; + +export { SearchResultsList }; diff --git a/packages/paste-website/src/components/site-search/SearchResultsLoading.tsx b/packages/paste-website/src/components/site-search/SearchResultsLoading.tsx new file mode 100644 index 0000000000..ec68fb3ad0 --- /dev/null +++ b/packages/paste-website/src/components/site-search/SearchResultsLoading.tsx @@ -0,0 +1,48 @@ +import { Box } from "@twilio-paste/box"; +import { SkeletonLoader } from "@twilio-paste/skeleton-loader"; +import * as React from "react"; + +export const SearchResultsLoading: React.FC = () => { + return ( + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/paste-website/src/components/site-search/index.tsx b/packages/paste-website/src/components/site-search/index.tsx new file mode 100644 index 0000000000..ed10084ae2 --- /dev/null +++ b/packages/paste-website/src/components/site-search/index.tsx @@ -0,0 +1,98 @@ +/* eslint-disable react/jsx-max-depth */ +import { type ModalProps } from "@twilio-paste/modal"; +import { useUID } from "@twilio-paste/uid-library"; +import groupBy from "lodash/groupBy"; +import * as React from "react"; + +import { SearchEmptyState } from "./SearchEmptyState"; +import { SearchForm } from "./SearchForm"; +import { SearchModal } from "./SearchModal"; +import { SearchResultsList } from "./SearchResults"; +import { SearchResultsLoading } from "./SearchResultsLoading"; +import { type GroupedSearchResults, type SearchItem, type SearchResults } from "./types"; + +const groupResults = (results: SearchItem[]): GroupedSearchResults => groupBy(results, "path"); + +export type SiteSearchProps = Pick; + +const SiteSearch: React.FC> = ({ isOpen, onDismiss }) => { + const modalHeadingId = useUID(); + + const [searchQuery, setSearchQuery] = React.useState(""); + const [results, setResults] = React.useState({}); + const [loading, setLoading] = React.useState(false); + const [searchInitialState, setSearchInitialState] = React.useState(true); + + const inputRef = React.useRef(null); + + const performSearch = React.useCallback(async (): Promise => { + if (searchQuery === "") { + return; + } + + setLoading(true); + + try { + const cachedResults = sessionStorage.getItem(searchQuery); + if (cachedResults) { + setResults(JSON.parse(cachedResults)); + } else { + const response = await fetch("/api/docs-search", { + method: "POST", + body: JSON.stringify({ prompt: searchQuery }), + }); + const json = (await response.json()) as SearchResults; + const groupedResults = groupResults(json.data); + setResults(groupedResults); + sessionStorage.setItem(searchQuery, JSON.stringify(groupedResults)); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + } + + setSearchInitialState(false); + setLoading(false); + }, [searchQuery]); + + React.useEffect(() => { + if (searchQuery === "") { + // reset the search state when we empty the search query to start again + setSearchInitialState(true); + } + }, [searchQuery]); + + const handleOnChange = (event: React.ChangeEvent): void => { + setSearchQuery(event.target.value); + }; + + const clearResults = (): void => { + setResults({}); + setSearchQuery(""); + inputRef.current?.focus(); + }; + + return ( + + 0} + onClear={clearResults} + ariaLabelledBy={modalHeadingId} + inputValue={searchQuery} + onChange={handleOnChange} + inputRef={inputRef} + /> + {loading && } + {searchQuery && !searchInitialState && !loading && Object.keys(results).length === 0 && ( + + )} + + + ); +}; + +SiteSearch.displayName = "SiteSearch"; + +export { SiteSearch }; +/* eslint-enable react/jsx-max-depth */ diff --git a/packages/paste-website/src/components/site-search/types.ts b/packages/paste-website/src/components/site-search/types.ts new file mode 100644 index 0000000000..5e12f0d5d5 --- /dev/null +++ b/packages/paste-website/src/components/site-search/types.ts @@ -0,0 +1,35 @@ +type CommonSearchItem = { + content: string; + heading: string; + path: string; + similarity: number; + source: string; + slug: string; + meta: { + title: string; + }; +}; +export type DocSearchItem = CommonSearchItem & { + type: "markdown"; + meta: { + description: string; + package: string; + slug: string; + }; +}; +export type DiscussionSearchItem = CommonSearchItem & { + type: "github-discussions"; + meta: { + id: string; + updatedAt: string; + }; +}; + +export type SearchItem = DocSearchItem | DiscussionSearchItem; + +export type SearchResults = { + data: SearchItem[]; +}; +export type GroupedSearchResults = { + [key: string]: SearchItem[]; +}; diff --git a/packages/paste-website/src/components/site-wrapper/SiteBody.tsx b/packages/paste-website/src/components/site-wrapper/SiteBody.tsx index bd5cf885cb..8681c2ecbe 100644 --- a/packages/paste-website/src/components/site-wrapper/SiteBody.tsx +++ b/packages/paste-website/src/components/site-wrapper/SiteBody.tsx @@ -11,7 +11,6 @@ import { SidebarPushContentWrapper, } from "@twilio-paste/sidebar"; import { type CSSObject, StylingGlobals } from "@twilio-paste/styling-library"; -import { useTheme } from "@twilio-paste/theme"; import { useWindowSize } from "@twilio-paste/utils"; import { useRouter } from "next/router"; import * as React from "react"; @@ -24,7 +23,6 @@ import { TOKEN_LIST_PAGE_REGEX, TOKEN_STICKY_FILTER_HEIGHT, } from "../../constants"; -import { docSearchStyles, docSearchVariable } from "../../styles/docSearch"; import { SiteMain } from "./SiteMain"; import { SidebarNavigation } from "./sidebar/SidebarNavigation"; import { SiteFooter } from "./site-footer"; @@ -43,7 +41,6 @@ const GlobalScrollBehaviourStyles = (scrollOffset = defaultScrollOffset): CSSObj export const SiteBody: React.FC = ({ children }) => { const { breakpointIndex } = useWindowSize(); - const themeObject = useTheme(); const router = useRouter(); // sidebar is not collapsed by default, most common use case for desktop viewing const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false); @@ -93,8 +90,6 @@ export const SiteBody: React.FC = ({ children }) => { {/** diff --git a/packages/paste-website/src/components/site-wrapper/site-header/DarkModeToggle.tsx b/packages/paste-website/src/components/site-wrapper/site-header/DarkModeToggle.tsx index e9b2884627..b14275ff7b 100644 --- a/packages/paste-website/src/components/site-wrapper/site-header/DarkModeToggle.tsx +++ b/packages/paste-website/src/components/site-wrapper/site-header/DarkModeToggle.tsx @@ -24,7 +24,9 @@ export const DarkModeToggle = (): JSX.Element => { <> event({ @@ -34,7 +36,11 @@ export const DarkModeToggle = (): JSX.Element => { }) } > - {theme === "twilio" ? : } + {theme === "twilio" ? ( + + ) : ( + + )} Switch the site theme diff --git a/packages/paste-website/src/components/site-wrapper/site-header/SiteHeaderSearch.tsx b/packages/paste-website/src/components/site-wrapper/site-header/SiteHeaderSearch.tsx index 0e2b8b45c4..a9022fba50 100644 --- a/packages/paste-website/src/components/site-wrapper/site-header/SiteHeaderSearch.tsx +++ b/packages/paste-website/src/components/site-wrapper/site-header/SiteHeaderSearch.tsx @@ -1,72 +1,72 @@ -import "@docsearch/css"; -import { DocSearchButton, useDocSearchKeyboardEvents } from "@docsearch/react"; -import type { DocSearchModal as DocSearchModalType } from "@docsearch/react"; -import Head from "next/head"; +import { Box } from "@twilio-paste/box"; +import { Button } from "@twilio-paste/button"; +import { SearchIcon } from "@twilio-paste/icons/esm/SearchIcon"; +import { InlineCode } from "@twilio-paste/inline-code"; +import { ScreenReaderOnly } from "@twilio-paste/screen-reader-only"; +import { Text } from "@twilio-paste/text"; import * as React from "react"; +import { useHotkeys } from "react-hotkeys-hook"; -import { DOCSEARCHV3_APIKEY, DOCSEARCHV3_APPID, DOCSEARCHV3_INDEXNAME } from "../../../constants"; - -// https://github.com/facebook/docusaurus/blob/main/packages/docusaurus-theme-search-algolia/src/theme/SearchBar/index.tsx -let DocSearchModal: typeof DocSearchModalType | null = null; +import { SiteSearch } from "../../site-search"; const SiteHeaderSearch: React.FC = () => { const [isOpen, setIsOpen] = React.useState(false); - const searchButtonRef = React.useRef(null); - - const importDocSearch = React.useCallback(async () => { - if (DocSearchModal) { - return; - } - const { DocSearchModal: Modal }: typeof import("@docsearch/react") = await import( - /* webpackChunkName: 'DocSearchModal' */ "@docsearch/react/modal" - ); - DocSearchModal = Modal; - }, []); - const onOpen = React.useCallback(async () => { - await importDocSearch(); + const onOpen = (): void => { setIsOpen(true); - }, [importDocSearch, setIsOpen]); + }; - const onClose = React.useCallback(() => { + const onClose = (): void => { setIsOpen(false); - }, [setIsOpen]); - - const onInput = React.useCallback(async () => { - await importDocSearch(); - setIsOpen(true); - }, [importDocSearch, setIsOpen]); + }; - useDocSearchKeyboardEvents({ - isOpen, - onOpen, - onClose, - onInput, - searchButtonRef, - }); + useHotkeys("mod+k", onOpen); return ( <> - - - - - {isOpen && DocSearchModal ? ( - - ) : null} + _hover={{ + boxShadow: "shadowBorderPrimary", + }} + _focus={{ + boxShadow: "shadowFocusShadowBorder", + }} + _active={{ + boxShadow: "shadowBorderPrimaryStronger", + }} + > + + + + + Search + + + + Keyboard shortcut: Command / Control K + + + ); }; diff --git a/packages/paste-website/src/components/site-wrapper/site-header/index.tsx b/packages/paste-website/src/components/site-wrapper/site-header/index.tsx index 2f5fcd0cd1..74dad17f5b 100644 --- a/packages/paste-website/src/components/site-wrapper/site-header/index.tsx +++ b/packages/paste-website/src/components/site-wrapper/site-header/index.tsx @@ -1,6 +1,7 @@ import { Box } from "@twilio-paste/box"; import { Button } from "@twilio-paste/button"; import { Topbar, TopbarActions } from "@twilio-paste/topbar"; +import { useWindowSize } from "@twilio-paste/utils"; import * as React from "react"; import GitHubButton from "react-github-button"; import "react-github-button/assets/style.css"; @@ -17,34 +18,38 @@ export const SiteHeader: React.FC<{ sidebarMobileCollapsed: boolean; setSidebarMobileCollapsed: (collapsed: boolean) => void; }> = ({ sidebarMobileCollapsed, setSidebarMobileCollapsed }): JSX.Element => { + const { breakpointIndex } = useWindowSize(); return ( - - - - - - - - - - - - - - - + {breakpointIndex === 0 ? ( + + + + + + + ) : ( + + + + + + + + + + )} ); }; diff --git a/packages/paste-website/src/constants.ts b/packages/paste-website/src/constants.ts index 10ab223261..0726e8a15e 100644 --- a/packages/paste-website/src/constants.ts +++ b/packages/paste-website/src/constants.ts @@ -3,11 +3,6 @@ export const TWILIO_BLUE = "#0D122B"; export const PSA_ALERT_HEIGHT = 40; export const PASTE_THEME_ALERT_HEIGHT = 54; export const SITE_TOPBAR_HEIGHT = 77; -/* - * Note: - * Changing the mobile breakpoint should be reflected - * in styles/docSearch.ts - */ export const SITE_BREAKPOINTS = ["768px", "1024px", "1220px", "1880px"]; export const SITE_CONTENT_MAX_WIDTH = "1440px"; @@ -33,10 +28,6 @@ export const TOKEN_LIST_PAGE_REGEX = /^\/tokens\/list$/; // env variables export const DATADOG_APPLICATION_ID = process.env.NEXT_PUBLIC_DATADOG_APPLICATION_ID || "no env variable"; export const DATADOG_CLIENT_TOKEN = process.env.NEXT_PUBLIC_DATADOG_CLIENT_TOKEN || "no env variable"; -export const DOCSEARCH_APIKEY = process.env.NEXT_PUBLIC_DOCSEARCH_APIKEY || "no env variable"; -export const DOCSEARCHV3_APIKEY = process.env.NEXT_PUBLIC_DOCSEARCHV3_APIKEY || "no env variable"; -export const DOCSEARCHV3_INDEXNAME = process.env.NEXT_PUBLIC_DOCSEARCHV3_INDEXNAME || "no env variable"; -export const DOCSEARCHV3_APPID = process.env.NEXT_PUBLIC_DOCSEARCHV3_APPID || "no env variable"; /* * Netlify provides an environment variable called CONTEXT which reflects their build context https://docs.netlify.com/site-deploys/overview/#deploy-contexts * We need to use this to know where the Next site is being run for metrics tracking. Next env variables all need diff --git a/packages/paste-website/src/pages/api/docs-search.ts b/packages/paste-website/src/pages/api/docs-search.ts index e4e9cadd2d..2373bd23a2 100644 --- a/packages/paste-website/src/pages/api/docs-search.ts +++ b/packages/paste-website/src/pages/api/docs-search.ts @@ -16,7 +16,6 @@ class ApplicationError extends Error { class UserError extends ApplicationError {} const openAiKey = process.env.OPENAI_API_KEY; -const openAiSecret = process.env.OPENAI_API_SECRET; const supabaseUrl = process.env.SUPABASE_URL; const supabaseServiceKey = process.env.SUPABASE_KEY; @@ -32,9 +31,6 @@ export default async function handler(req: NextRequest): Promise { - return { - ":root": { - "--docsearch-primary-color": theme.backgroundColors.colorBackgroundPrimaryStronger, - "--docsearch-text-color": theme.textColors.colorText, - "--docsearch-spacing": theme.space.space50, - "--docsearch-icon-stroke-width": theme.borderWidths.borderWidth20, - "--docsearch-highlight-color": theme.backgroundColors.colorBackgroundPrimaryStronger, - "--docsearch-muted-color": theme.textColors.colorTextWeak, - "--docsearch-container-background": theme.backgroundColors.colorBackgroundOverlay, - "--docsearch-logo-color": theme.textColors.colorTextWeak, - "--docsearch-modal-width": theme.sizes.size70, - "--docsearch-modal-height": "600px", - "--docsearch-modal-background": theme.backgroundColors.colorBackgroundBody, - "--docsearch-modal-shadow": theme.shadows.shadowCard, - "--docsearch-searchbox-height": "46px", - "--docsearch-searchbox-background": theme.backgroundColors.colorBackgroundInverse, - "--docsearch-searchbox-focus-background": theme.backgroundColors.colorBackgroundBody, - "--docsearch-searchbox-shadow": theme.shadows.shadowFocus, - "--docsearch-hit-height": "56px", - "--docsearch-hit-color": theme.textColors.colorText, - "--docsearch-hit-active-color": theme.textColors.colorTextWeakest, - "--docsearch-hit-background": theme.backgroundColors.colorBackgroundBody, - "--docsearch-hit-shadow": "none", - "--docsearch-key-gradient": theme.backgroundColors.colorBackground, - "--docsearch-key-shadow": "none", - "--docsearch-footer-height": "44px", - "--docsearch-footer-background": theme.backgroundColors.colorBackgroundBody, - "--docsearch-footer-shadow": "none", - }, - }; -}; -export const docSearchStyles = css({ - ".DocSearch-Button": { - backgroundColor: "colorBackgroundBody", - borderRadius: "borderRadius20", - boxShadow: "shadowBorder", - color: "colorTextWeak", - paddingX: "space40", - margin: "0", - width: "100%", - "&:hover": { - boxShadow: "shadowBorderPrimary", - }, - "&:active, &:focus": { - boxShadow: "shadowFocusShadowBorder", - }, - ".DocSearch-Search-Icon": { - color: "colorTextIcon", - size: "sizeIcon10", - }, - }, - ".DocSearch-Button-Placeholder": { - fontSize: "fontSize30", - fontFamily: "fontFamilyText", - fontWeight: "fontWeightMedium", - fontStyle: "italic", - paddingX: "space40", - }, - ".DocSearch-Button-Key": { - top: 0, - paddingBottom: 0, - }, - ".DocSearch-Container": { - ...pasteBaseStyles(), - }, - ".DocSearch-Dropdown": { - a: { - color: "colorTextLink", - textDecoration: "underline", - ":hover": { - textDecoration: "none", - }, - }, - }, - ".DocSearch-SearchBar": { - paddingBottom: "space20", - }, - ".DocSearch-Search-Icon": { - color: "colorTextIcon", - }, - ".DocSearch-Input": { - paddingLeft: "space50", - }, - ".DocSearch-LoadingIndicator svg, .DocSearch-MagnifierLabel svg": { - size: "sizeIcon10", - }, - ".DocSearch-Hits mark": { - color: "colorTextLink", - }, - ".DocSearch-Hit": { - paddingBottom: "space30", - a: { - borderColor: "colorBorderWeaker", - borderStyle: "solid", - borderWidth: "borderWidth20", - textDecoration: "none", - }, - "&[aria-selected=true] a": { - borderColor: "colorBorderPrimaryStronger", - }, - }, - ".DocSearch-Hit-title": { - fontSize: "fontSize30", - }, - ".DocSearch-Hit-path": { - fontSize: "fontSize20", - }, - ".DocSearch-Hit-source": { - color: "colorText", - fontSize: "fontSize30", - fontWeight: "fontWeightMedium", - }, - ".DocSearch-Hit-icon": { - display: "none", - }, - ".DocSearch-Prefill": { - color: "colorTextLink", - textDecoration: "underline", - "&:hover": { - textDecoration: "none", - }, - }, - ".DocSearch-Commands-Key": { - paddingBottom: "0", - }, -}); diff --git a/packages/paste-website/stories/SiteSearch.stories.tsx b/packages/paste-website/stories/SiteSearch.stories.tsx new file mode 100644 index 0000000000..89b89ccd17 --- /dev/null +++ b/packages/paste-website/stories/SiteSearch.stories.tsx @@ -0,0 +1,75 @@ +import { type StoryFn } from "@storybook/react"; +import { Box } from "@twilio-paste/box"; +import { useUID } from "@twilio-paste/uid-library"; +import * as React from "react"; + +import { SearchEmptyState } from "../src/components/site-search/SearchEmptyState"; +import { SearchForm } from "../src/components/site-search/SearchForm"; +import { SearchResultsList } from "../src/components/site-search/SearchResults"; +import { SearchResultsLoading } from "../src/components/site-search/SearchResultsLoading"; +import { type GroupedSearchResults } from "../src/components/site-search/types"; +import { SiteHeaderSearch } from "../src/components/site-wrapper/site-header/SiteHeaderSearch"; +import { mockResults } from "./__fixtures__/searchresults.fixture"; + +const MockSearchModal: React.FC<{ + results: GroupedSearchResults; + searchQuery: string; + loading?: boolean; + showEmptyState?: boolean; +}> = ({ results, searchQuery, loading, showEmptyState }) => { + const modalHeadingId = useUID(); + const inputRef = React.useRef(null); + return ( + // we don't need to VRT a modal, we know what they look like else where, we only care about the content + + {}} + hasResults={Object.keys(results).length > 0} + onClear={() => {}} + ariaLabelledBy={modalHeadingId} + inputValue={searchQuery} + onChange={() => {}} + inputRef={inputRef} + /> + {loading && Object.keys(results).length === 0 && } + {showEmptyState && Object.keys(results).length === 0 && } + + + ); +}; +export const SiteSearchTrigger: StoryFn = () => { + return ; +}; +SiteSearchTrigger.parameters = { + a11y: { + config: { + rules: [ + { + id: "color-contrast", + // This uses a symbol as text content and even though it's ignored aXe can't cope with figuring out if it's accessible or not + enabled: false, + }, + ], + }, + }, +}; + +export const SiteSearchModalInitialState: StoryFn = () => { + return ; +}; + +export const SiteSearchModalLoading: StoryFn = () => { + return ; +}; + +export const SiteSearchModalEmptyState: StoryFn = () => { + return ; +}; + +export const SiteSearchModalResultsState: StoryFn = () => { + return ; +}; + +export default { + title: "Website/SiteSearch", +}; diff --git a/packages/paste-website/stories/__fixtures__/searchresults.fixture.ts b/packages/paste-website/stories/__fixtures__/searchresults.fixture.ts new file mode 100644 index 0000000000..dd4acb82b9 --- /dev/null +++ b/packages/paste-website/stories/__fixtures__/searchresults.fixture.ts @@ -0,0 +1,155 @@ +import type { GroupedSearchResults } from "../../src/components/site-search/types"; + +export const mockResults = { + "src/pages/components/in-page-navigation/index": [ + { + path: "src/pages/components/in-page-navigation/index", + content: + "### About In Page Navigation\n\nThe In Page Navigation component allows users to navigate to different pages using a styled group of links. Each In Page Navigation Item is an anchor that links to a related page. Each page within the In Page Navigation should also have an In Page Navigation component so that users can easily navigate back and forth within a set of related pages.\n", + similarity: 0.856512844562531, + source: "docs", + type: "markdown", + meta: { + slug: "/components/in-page-navigation/", + title: "In Page Navigation", + package: "@twilio-paste/in-page-navigation", + description: "An In Page Navigation is a group of styled links that lets users navigate between related pages.", + }, + heading: "About In Page Navigation", + slug: "about-in-page-navigation", + }, + { + path: "src/pages/components/in-page-navigation/index", + content: + "### In Page Navigation vs Tabs\n\n[Tabs](/components/tabs) are used to layer related pieces of information together and display one layer at a time on the same URL. Use Tabs to alternate between views within the same context. In Page Navigation is a collection of anchors, rather than buttons. Use In Page Navigation to switch between different, related pages. Tabs replace the entire view based on the selected tab. In Page Navigation links navigate the user to a new page.\n", + similarity: 0.853896617889404, + source: "docs", + type: "markdown", + meta: { + slug: "/components/in-page-navigation/", + title: "In Page Navigation", + package: "@twilio-paste/in-page-navigation", + description: "An In Page Navigation is a group of styled links that lets users navigate between related pages.", + }, + heading: "In Page Navigation vs Tabs", + slug: "in-page-navigation-vs-tabs", + }, + { + path: "src/pages/components/in-page-navigation/index", + content: + "### Accessibility\n\n* Each In Page Navigation must have a unique label. To add the label, add the `aria-label` prop to the `` tag.\n Omit the term 'navigation'- it is redundant since the role is already defined as 'navigation'.\n* To interact with In Page Navigation using the keyboard, use the tab key.\n* Each In Page Navigation must have an In Page Navigation Item which is the currently selected page. To specify which page is current, add the `currentPage` prop to the respective ``. Doing so will set `aria-current=\"page\"` on that link.\n", + similarity: 0.824576377868652, + source: "docs", + type: "markdown", + meta: { + slug: "/components/in-page-navigation/", + title: "In Page Navigation", + package: "@twilio-paste/in-page-navigation", + description: "An In Page Navigation is a group of styled links that lets users navigate between related pages.", + }, + heading: "Accessibility", + slug: "accessibility", + }, + ], + "src/pages/components/tabs/index": [ + { + path: "src/pages/components/tabs/index", + content: + "### Tabs vs. In Page Navigation\n\nTabs are used to layer related pieces of information together and display one layer at a time on the same URL. Use Tabs to alternate between views within the same context. [In Page Navigation](/components/in-page-navigation) is a collection of anchors, rather than buttons. Use In Page Navigation to switch between different, related pages. Tabs replace the entire view based on the selected tab. In Page Navigation links navigate the user to a new page.\n", + similarity: 0.845560073852539, + source: "docs", + type: "markdown", + meta: { + slug: "/components/tabs/", + title: "Tabs", + package: "@twilio-paste/tabs", + description: "Tabs are labeled controls that allow users to switch between multiple views within a page.", + }, + heading: "Tabs vs. In Page Navigation", + slug: "tabs-vs-in-page-navigation", + }, + ], + "https://github.com/twilio-labs/paste/discussions/3356": [ + { + path: "https://github.com/twilio-labs/paste/discussions/3356", + content: + '# switching pages in the breadcrumbs\nhey, we are trying to reproduce patter that is being used in Evergreen. It\'s a breadcrumb combined with minimal button which - once we click - opens a list and we can jump into the another page. \r\n\r\nAny suggestions how to handle this type of case?\r\n\r\nZrzut ekranu 2023-07-27 o 09 11 40\r\n', + similarity: 0.840333878993988, + source: "twilio-labs/paste", + type: "github-discussions", + meta: { + id: "D_kwDOC9UAQs4AUxlu", + title: "switching pages in the breadcrumbs", + updatedAt: "2023-08-03T16:48:57Z", + }, + heading: "switching pages in the breadcrumbs", + slug: "discussion-5445998", + }, + ], + "https://github.com/twilio-labs/paste/discussions/3222": [ + { + path: "https://github.com/twilio-labs/paste/discussions/3222", + content: + "# Page interaction on click for pagination - Answer\nHi @tonge495 \n\nI don't think we have a particular preference, it's largely based on user expectations based on what you're actually doing, aligning to default browser behaviours.\n\nThere are largely two approaches:\n\n1) Each page in the pagination takes the user to a new route/url. When visiting that route the table of results will always be the same. That way you can share your search query and results (Preferred). In this case the action is a page navigation, just like navigating to any other page, and should act accordingly. This usually takes a user to the very top of the page.\n\n2) Each page in the pagination just re-renders the table above. No route change. In this case you must manually place the user above the table of results to inform them that something on the page has happened. \n\nIn both cases, whether you jump or soft scroll to the new location is up to you, and potentially what action is being taken in the browser, will dictate what you _can_ do.\n\nHope that helps a little.", + similarity: 0.832261383533478, + source: "twilio-labs/paste", + type: "github-discussions", + meta: { + id: "D_kwDOC9UAQs4ATwEp", + title: "Page interaction on click for pagination", + updatedAt: "2023-05-10T20:22:07Z", + }, + heading: "Page interaction on click for pagination - Answer", + slug: "discussion-answer-5177641", + }, + ], + "https://github.com/twilio-labs/paste/discussions/2337": [ + { + path: "https://github.com/twilio-labs/paste/discussions/2337", + content: + '# Buttons with link functionality\nHi Team!\r\n\r\nI have some questions about buttons with link functionality.\r\nThe paste guidelines suggest to use buttons with link functionality when we want to navigate the user, the addition of the icons help indicate the action performed on click is a navigation.\r\nIs it possible to get more context around the term "navigate"?\r\n\r\nDoes navigate mean:\r\n- going somewhere else on the same page\r\n- going to a completely new page\r\n- going from one dedicated place in the side navigation to another\r\n- displaying new UI\r\n\r\nI\'ve looked at other parts of console, and navigate means that you are going from one dedicated place in the side navigation to another. The A2P homepage currently uses buttons with link functionality and I just want to make sure we are utilizing them in the correct way.\r\n\r\nThank you so much for all your help! ', + similarity: 0.829035758972168, + source: "twilio-labs/paste", + type: "github-discussions", + meta: { + id: "D_kwDOC9UAQs4APLzh", + title: "Buttons with link functionality", + updatedAt: "2022-07-20T09:29:56Z", + }, + heading: "Buttons with link functionality", + slug: "discussion-3980513", + }, + { + path: "https://github.com/twilio-labs/paste/discussions/2337", + content: + '# Buttons with link functionality - Answer\nHello again, @jkiga 👋 \r\n\r\nWe define **navigate** to mean any **route based URL change**.\r\n\r\nSo taking a look at your list of examples 👇 \r\n> going somewhere else on the same page ✅\r\n\r\nThis is only navigation when that change in position (e.g. "going somewhere") is caused by a change in location (e.g the URL). For example, in-page navigation using `id` attributes and matching hashed `href` attributes on the anchor.\r\n
\r\n> going to a completely new page ✅\r\n\r\nIf the change in page implies a change in location (URL), then yes.\r\n
\r\n> going from one dedicated place in the side navigation to another ✅\r\n\r\nClicking on a navigation item and being redirected to a new location (e.g, new page new URL), is in fact navigation.\r\n
\r\n> displaying new UI 🟨 \r\n\r\nNot necessarily. You can couple page content/UI to location, but they are not necessarily always coupled.\r\n', + similarity: 0.826784372329712, + source: "twilio-labs/paste", + type: "github-discussions", + meta: { + id: "D_kwDOC9UAQs4APLzh", + title: "Buttons with link functionality", + updatedAt: "2022-07-20T09:29:56Z", + }, + heading: "Buttons with link functionality - Answer", + slug: "discussion-answer-3980513", + }, + ], + "https://github.com/twilio-labs/paste/discussions/3307": [ + { + path: "https://github.com/twilio-labs/paste/discussions/3307", + content: + '# No vertical in-page navigation\nHello team, \r\n\r\nPer paste guidance we should be using in-page navigation instead of tabs if we are navigating user to a different URL per tab. \r\nFor Segment-> Unify settings we need both a horizontal in-page navigation and a vertical in-page navigation. However it seems like we don\'t have a vertical in-page navigation per my ENG team\'s investigation. Could you confirm if that\'s true?\r\n\r\nIf that\'s the case, should we use vertical tabs instead or use 2 horizontal in-page navigation? I personally think two horizontal in-page navigation is bad UX. \r\n\r\nThank you so much for your help!\r\n\r\nScreenshot 2023-07-05 at 1 38 05 PM\r\n', + similarity: 0.827467799186707, + source: "twilio-labs/paste", + type: "github-discussions", + meta: { + id: "D_kwDOC9UAQs4AUfk9", + title: "No vertical in-page navigation", + updatedAt: "2023-08-02T17:18:19Z", + }, + heading: "No vertical in-page navigation", + slug: "discussion-5372221", + }, + ], +} as GroupedSearchResults; diff --git a/packages/paste-website/types/index.d.ts b/packages/paste-website/types/index.d.ts index 37b8fed319..ec312c71b6 100644 --- a/packages/paste-website/types/index.d.ts +++ b/packages/paste-website/types/index.d.ts @@ -19,4 +19,3 @@ declare module '*.mdx' { } } -declare module '@docsearch/react/modal'; diff --git a/yarn.lock b/yarn.lock index 9fc9b24d8b..1d5466c509 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,166 +12,6 @@ __metadata: languageName: node linkType: hard -"@algolia/autocomplete-core@npm:1.7.4": - version: 1.7.4 - resolution: "@algolia/autocomplete-core@npm:1.7.4" - dependencies: - "@algolia/autocomplete-shared": 1.7.4 - checksum: cd7c0badec2dd7f32eb1c567e740473df41d0b5cfdc009efc2b44d2c72e30d90a05882ca0616d6dc29326177d5183a7fd9c6189e5eab3abe26936e232ac5f43a - languageName: node - linkType: hard - -"@algolia/autocomplete-preset-algolia@npm:1.7.4": - version: 1.7.4 - resolution: "@algolia/autocomplete-preset-algolia@npm:1.7.4" - dependencies: - "@algolia/autocomplete-shared": 1.7.4 - peerDependencies: - "@algolia/client-search": ">= 4.9.1 < 6" - algoliasearch: ">= 4.9.1 < 6" - checksum: 4ea134757d611d1b7489f34b4366d103fb981dde3f75f39762fb71142f23bd024825f7541ab756ead9c87e223184616fd74b7762982054c96927fecd5a6e6e3e - languageName: node - linkType: hard - -"@algolia/autocomplete-shared@npm:1.7.4": - version: 1.7.4 - resolution: "@algolia/autocomplete-shared@npm:1.7.4" - checksum: d304b1e3523ccf36a4a21ef9c116c83360fc1bffc595e888f05c35ab00de293104184dafebd9b9ed8ac5ffa5c416ddd4b1139e9794a253f52863c1ae544c2c9c - languageName: node - linkType: hard - -"@algolia/cache-browser-local-storage@npm:4.13.0": - version: 4.13.0 - resolution: "@algolia/cache-browser-local-storage@npm:4.13.0" - dependencies: - "@algolia/cache-common": 4.13.0 - checksum: ad02bf64342f5df1c14713566e060afaf3a7c272f8e66cf19d09628ed2deb6621119f92347aa3f914528bc0d50561786e36e5e519b0ae274de6b39de518b313e - languageName: node - linkType: hard - -"@algolia/cache-common@npm:4.13.0": - version: 4.13.0 - resolution: "@algolia/cache-common@npm:4.13.0" - checksum: 04520b56579e0b67ba53e7fbb77d51a9edc1e85b7bbafbb114ec84dd104ec3f29f87e54d5e8c348dcf22f00c224ee69d4323acd262e5c1b7444b4036b0441634 - languageName: node - linkType: hard - -"@algolia/cache-in-memory@npm:4.13.0": - version: 4.13.0 - resolution: "@algolia/cache-in-memory@npm:4.13.0" - dependencies: - "@algolia/cache-common": 4.13.0 - checksum: 3e1357d679cc665a375fc3752946fbe951af67b9a66df0fa4f14e68522f6ce45f2849f3cddf9cf64a8bac1d734f0b6efb1a90209a6589f4a65d837b080dd6729 - languageName: node - linkType: hard - -"@algolia/client-account@npm:4.13.0": - version: 4.13.0 - resolution: "@algolia/client-account@npm:4.13.0" - dependencies: - "@algolia/client-common": 4.13.0 - "@algolia/client-search": 4.13.0 - "@algolia/transporter": 4.13.0 - checksum: ccb4e98b9ea0bbe56a9f4e633c409b62a8be151e550a86abe3bf19266f5e9313a0e9aa16c4da1122deff2bebdc174e1a33e2d807111dd07113afad81304855e4 - languageName: node - linkType: hard - -"@algolia/client-analytics@npm:4.13.0": - version: 4.13.0 - resolution: "@algolia/client-analytics@npm:4.13.0" - dependencies: - "@algolia/client-common": 4.13.0 - "@algolia/client-search": 4.13.0 - "@algolia/requester-common": 4.13.0 - "@algolia/transporter": 4.13.0 - checksum: 315d4a26e261fa868ecf3869f204ee712cb03ab36c33c9a0a181405485d639305623dfd683b5d2e290cd3511315bee449541a512b9e4375592d0aa9838514328 - languageName: node - linkType: hard - -"@algolia/client-common@npm:4.13.0": - version: 4.13.0 - resolution: "@algolia/client-common@npm:4.13.0" - dependencies: - "@algolia/requester-common": 4.13.0 - "@algolia/transporter": 4.13.0 - checksum: 00b467b58f884cf8c037acb4f473e4a0ae97af0a357741004d3241e07f63aa2b3a1a736e474fe98c85c2a03a4772903c5da2843b3364dbbb566d482aa4e47d31 - languageName: node - linkType: hard - -"@algolia/client-personalization@npm:4.13.0": - version: 4.13.0 - resolution: "@algolia/client-personalization@npm:4.13.0" - dependencies: - "@algolia/client-common": 4.13.0 - "@algolia/requester-common": 4.13.0 - "@algolia/transporter": 4.13.0 - checksum: e93afa1036bb5b5f2f3e78d11cef55436e21e9f8efdd9d191e96e6be5cc078bda5fedcffa79037b6b1874f1582f17ae14e11e4bca13d254dfcee76966f0be6ae - languageName: node - linkType: hard - -"@algolia/client-search@npm:4.13.0": - version: 4.13.0 - resolution: "@algolia/client-search@npm:4.13.0" - dependencies: - "@algolia/client-common": 4.13.0 - "@algolia/requester-common": 4.13.0 - "@algolia/transporter": 4.13.0 - checksum: 0a14029d2ea6b0fbb0337eee63268e8014d3075c590c17812df4fadafa3c3f5a7daa8dee5adb5258fcc3d9c0855cf54b1925b3af4ce8b771369911d121acd40a - languageName: node - linkType: hard - -"@algolia/logger-common@npm:4.13.0": - version: 4.13.0 - resolution: "@algolia/logger-common@npm:4.13.0" - checksum: 11a6ee5d380b4f1f1c09971e9ef9796e328959ddf23a0bb5e66867a31efe0337f0c1666f062d5ff9f287efdec44c00f799df24fb6cbd182773fb1e7b3e94ff16 - languageName: node - linkType: hard - -"@algolia/logger-console@npm:4.13.0": - version: 4.13.0 - resolution: "@algolia/logger-console@npm:4.13.0" - dependencies: - "@algolia/logger-common": 4.13.0 - checksum: ee5bae8b5165bd9c4c90b3c4c0560f04f836494a4cee1be61725235598707ef22a8173283673613c93d211ba25b7602c693060bea313aea3aecf47fd5979ed94 - languageName: node - linkType: hard - -"@algolia/requester-browser-xhr@npm:4.13.0": - version: 4.13.0 - resolution: "@algolia/requester-browser-xhr@npm:4.13.0" - dependencies: - "@algolia/requester-common": 4.13.0 - checksum: cc1baf68ef5b30db584c07f07a9a3ebde67fc9fca4d565cb8567603f67861d515fa7f301be1c33929bb20f338d412a8d4c82319cdb7b7e8c4e68898389faa650 - languageName: node - linkType: hard - -"@algolia/requester-common@npm:4.13.0": - version: 4.13.0 - resolution: "@algolia/requester-common@npm:4.13.0" - checksum: 3c12613b2b31c7b67406904c919ad746653033a2d330eaaa37664c37bda764362d2370ed3e12d9ef9257c4f3bfc0ede92d2aaf3b25aa68023f44f487bcb23926 - languageName: node - linkType: hard - -"@algolia/requester-node-http@npm:4.13.0": - version: 4.13.0 - resolution: "@algolia/requester-node-http@npm:4.13.0" - dependencies: - "@algolia/requester-common": 4.13.0 - checksum: b708a96ba56e56155b6167ee40ea24de382d7e3148207da576fccd39dfd7f48bbf01a40a16899038d436a870e7f3a46863fc69e2ef84155ae54e37b9299cd01b - languageName: node - linkType: hard - -"@algolia/transporter@npm:4.13.0": - version: 4.13.0 - resolution: "@algolia/transporter@npm:4.13.0" - dependencies: - "@algolia/cache-common": 4.13.0 - "@algolia/logger-common": 4.13.0 - "@algolia/requester-common": 4.13.0 - checksum: a9e342872f2234ca50aadb513b938bd10e864f1c718190ac1c2b721389dbff8de64bd3e60a484b341a9307cbc97e1fc7802db481558f7663312c70a209947862 - languageName: node - linkType: hard - "@ampproject/remapping@npm:^2.2.0": version: 2.2.1 resolution: "@ampproject/remapping@npm:2.2.1" @@ -2772,36 +2612,6 @@ __metadata: languageName: node linkType: hard -"@docsearch/css@npm:3.3.3, @docsearch/css@npm:^3.3.3": - version: 3.3.3 - resolution: "@docsearch/css@npm:3.3.3" - checksum: c3e678dd5e05a962d3e29b4c953632a013af3a352ad99d0e630546409e665684e122265034bca1619d9bd659e42d35c7cc90ee373836fcfb2614aae2057c5dc1 - languageName: node - linkType: hard - -"@docsearch/react@npm:^3.3.3": - version: 3.3.3 - resolution: "@docsearch/react@npm:3.3.3" - dependencies: - "@algolia/autocomplete-core": 1.7.4 - "@algolia/autocomplete-preset-algolia": 1.7.4 - "@docsearch/css": 3.3.3 - algoliasearch: ^4.0.0 - peerDependencies: - "@types/react": ">= 16.8.0 < 19.0.0" - react: ">= 16.8.0 < 19.0.0" - react-dom: ">= 16.8.0 < 19.0.0" - peerDependenciesMeta: - "@types/react": - optional: true - react: - optional: true - react-dom: - optional: true - checksum: 8a31c175853b61ee80748abc0cebdc33d247483643c4151a430e05d37f159bf59ea08cb69f878cff7787d3ca122b664701575543914d3c3692b448b63d3ad716 - languageName: node - linkType: hard - "@emotion/babel-plugin-jsx-pragmatic@npm:^0.1.5": version: 0.1.5 resolution: "@emotion/babel-plugin-jsx-pragmatic@npm:0.1.5" @@ -13780,8 +13590,6 @@ __metadata: resolution: "@twilio-paste/website@workspace:packages/paste-website" dependencies: "@datadog/browser-rum": ^4.46.0 - "@docsearch/css": ^3.3.3 - "@docsearch/react": ^3.3.3 "@hookform/error-message": 2.0.0 "@mdx-js/loader": ^1.6.22 "@mdx-js/mdx": ^1.6.22 @@ -13792,6 +13600,7 @@ __metadata: "@octokit/core": ^5.0.1 "@octokit/plugin-paginate-graphql": ^4.0.0 "@sparticuz/chromium": ^110.0.0 + "@storybook/react": 7.0.6 "@supabase/supabase-js": ^2.36.0 "@testing-library/react": ^13.4.0 "@twilio-paste/account-switcher": ^3.0.0 @@ -13932,6 +13741,7 @@ __metadata: react-dom: ^18.0.0 react-github-button: ^0.1.11 react-hook-form: ^7.30.0 + react-hotkeys-hook: ^4.4.1 react-live: ^3.1.1 react-scrollspy: ^3.4.0 react-visibility-sensor: 5.1.1 @@ -16152,28 +15962,6 @@ __metadata: languageName: node linkType: hard -"algoliasearch@npm:^4.0.0": - version: 4.13.0 - resolution: "algoliasearch@npm:4.13.0" - dependencies: - "@algolia/cache-browser-local-storage": 4.13.0 - "@algolia/cache-common": 4.13.0 - "@algolia/cache-in-memory": 4.13.0 - "@algolia/client-account": 4.13.0 - "@algolia/client-analytics": 4.13.0 - "@algolia/client-common": 4.13.0 - "@algolia/client-personalization": 4.13.0 - "@algolia/client-search": 4.13.0 - "@algolia/logger-common": 4.13.0 - "@algolia/logger-console": 4.13.0 - "@algolia/requester-browser-xhr": 4.13.0 - "@algolia/requester-common": 4.13.0 - "@algolia/requester-node-http": 4.13.0 - "@algolia/transporter": 4.13.0 - checksum: 58b9deacb5c9b3b0cd045d519dde805b8e069ce8b524a280a858e06d848ddf6ef8be1871d0323bf87e0b4e939ca469ee489be84926d2fcf387a2561ee74ddbec - languageName: node - linkType: hard - "ansi-align@npm:^2.0.0": version: 2.0.0 resolution: "ansi-align@npm:2.0.0" @@ -36632,6 +36420,16 @@ fsevents@^1.2.7: languageName: node linkType: hard +"react-hotkeys-hook@npm:^4.4.1": + version: 4.4.1 + resolution: "react-hotkeys-hook@npm:4.4.1" + peerDependencies: + react: ">=16.8.1" + react-dom: ">=16.8.1" + checksum: c03d5d013ec41f3a63a3dd7d704d2fc98fba227811f7a1222af8651705f1cef983b7ccc4325f9910fec920156b14b4f05101882dc7d0bde1601ba56c01d5f162 + languageName: node + linkType: hard + "react-inspector@npm:^6.0.0": version: 6.0.1 resolution: "react-inspector@npm:6.0.1"