From 4d1511a63c8b3bc9ada577f81f74c10668e6b5e4 Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Sat, 6 Jul 2024 16:09:56 -0400 Subject: [PATCH] feat: Cohere Docs Theme (#1105) --- package.json | 2 - packages/ui/app/package.json | 2 +- packages/ui/app/src/__test__/setup.ts | 6 +- packages/ui/app/src/api-page/ApiPage.tsx | 18 +- .../src/api-page/ApiSectionMarkdownPage.tsx | 60 +- .../src/api-page/artifacts/ApiArtifacts.tsx | 2 +- .../app/src/api-page/endpoints/Endpoint.tsx | 32 +- .../api-page/endpoints/EndpointContent.tsx | 230 +-- .../endpoints/EndpointContentCodeSnippets.tsx | 5 +- .../api-page/endpoints/EndpointParameter.tsx | 2 +- .../api-page/endpoints/EndpointSection.tsx | 2 +- .../EndpointStreamingEnabledToggle.tsx | 36 + .../api-page/endpoints/ErrorExampleSelect.tsx | 4 +- .../src/api-page/examples/TitledExample.tsx | 6 +- .../api-page/subpackages/ApiSubpackage.tsx | 5 +- .../api-page/types/object/ObjectProperty.tsx | 4 +- .../src/api-page/useApiPageCenterElement.ts | 11 +- .../app/src/api-page/web-socket/WebSocket.tsx | 10 +- .../src/api-page/webhooks/WebhookContent.tsx | 10 +- .../src/api-playground/PlaygroundContext.tsx | 3 - .../src/api-playground/PlaygroundDrawer.tsx | 36 +- .../PlaygroundEndpointContent.tsx | 8 +- .../api-playground/PlaygroundEndpointPath.tsx | 7 + packages/ui/app/src/atoms/flags.ts | 2 + packages/ui/app/src/atoms/layout.ts | 96 +- packages/ui/app/src/atoms/location.ts | 21 + packages/ui/app/src/atoms/logo.ts | 3 + packages/ui/app/src/atoms/navigation.ts | 42 +- packages/ui/app/src/atoms/playground.ts | 31 +- packages/ui/app/src/atoms/sidebar.ts | 81 + packages/ui/app/src/atoms/viewport.ts | 155 ++ packages/ui/app/src/atoms/window.ts | 35 - .../commons/AbsolutelyPositionedAnchor.scss | 6 +- .../commons/AbsolutelyPositionedAnchor.tsx | 15 +- .../src/commons/HorizontalOverflowMask.tsx | 6 +- .../components/BottomNavigationButtons.tsx | 4 +- .../{api-page => components}/Breadcrumbs.tsx | 0 .../ui/app/src/components/EditThisPage.tsx | 22 + .../app/src/components/FernErrorBoundary.tsx | 8 +- packages/ui/app/src/components/FernLink.tsx | 10 +- packages/ui/app/src/components/PageHeader.tsx | 33 + .../TableOfContents.tsx | 2 +- .../src/contexts/docs-context/DocsContext.ts | 4 + .../docs-context/DocsContextProvider.tsx | 15 +- .../layout-breakpoint/LayoutBreakpoint.ts | 24 - .../LayoutBreakpointProvider.tsx | 99 -- .../layout-breakpoint/useLayoutBreakpoint.ts | 10 - .../navigation-context/NavigationContext.ts | 19 - .../NavigationContextProvider.tsx | 58 +- .../useNavigationContext.ts | 13 +- .../ui/app/src/contexts/useApiPageContext.ts | 7 + packages/ui/app/src/contexts/useIsReady.tsx | 22 - packages/ui/app/src/css/base.scss | 2 +- packages/ui/app/src/css/components.scss | 14 +- packages/ui/app/src/css/globals.scss | 4 + packages/ui/app/src/css/utilities.scss | 52 + .../src/custom-docs-page/CustomDocsPage.tsx | 31 - .../ui/app/src/custom-docs-page/Feedback.tsx | 9 +- packages/ui/app/src/docs/ChangelogPage.tsx | 29 +- packages/ui/app/src/docs/Docs.tsx | 98 -- packages/ui/app/src/docs/DocsMainContent.tsx | 53 +- packages/ui/app/src/docs/Header.scss | 17 + packages/ui/app/src/docs/Header.tsx | 71 +- packages/ui/app/src/docs/HeaderContainer.tsx | 78 - .../ui/app/src/docs/HeaderLogoSection.css | 3 - .../ui/app/src/docs/HeaderLogoSection.scss | 3 + .../ui/app/src/docs/HeaderLogoSection.tsx | 150 +- packages/ui/app/src/docs/HeaderTabs.tsx | 39 +- packages/ui/app/src/docs/MobileMenuButton.tsx | 28 + packages/ui/app/src/docs/useIsScrolled.ts | 2 +- packages/ui/app/src/index.ts | 3 +- packages/ui/app/src/layout/CustomLayout.tsx | 20 + packages/ui/app/src/layout/GuideLayout.tsx | 80 + packages/ui/app/src/layout/OverviewLayout.tsx | 74 + packages/ui/app/src/layout/PageLayout.tsx | 65 + .../ui/app/src/layout/ReferenceLayout.tsx | 83 + packages/ui/app/src/mdx/Markdown.tsx | 6 +- packages/ui/app/src/mdx/MdxContent.tsx | 15 +- packages/ui/app/src/mdx/base-components.tsx | 13 +- .../app/src/mdx/components/AccordionGroup.tsx | 2 +- .../ui/app/src/mdx/components/CodeGroup.tsx | 2 +- .../mdx/components/HTMLTable/HTMLTable.tsx | 1 + packages/ui/app/src/mdx/components/IFrame.tsx | 1 + .../app/src/mdx/components/RequestSnippet.tsx | 6 +- packages/ui/app/src/mdx/components/Steps.scss | 6 +- packages/ui/app/src/mdx/components/Tabs.tsx | 2 +- .../ui/app/src/mdx/frontmatter-context.tsx | 10 + packages/ui/app/src/mdx/frontmatter.ts | 134 ++ packages/ui/app/src/mdx/mdx-components.tsx | 18 +- packages/ui/app/src/mdx/mdx.ts | 82 +- packages/ui/app/src/mdx/plugins/makeToc.ts | 25 +- .../ui/app/src/mdx/plugins/rehypeLayout.ts | 208 +-- packages/ui/app/src/mdx/plugins/to-estree.ts | 6 +- packages/ui/app/src/mdx/plugins/utils.ts | 9 + packages/ui/app/src/next-app/DocsPage.tsx | 35 +- packages/ui/app/src/next-app/NextApp.tsx | 19 +- .../src/next-app/utils/getBreadcrumbList.ts | 4 +- .../ui/app/src/next-app/utils/getSeoProp.ts | 14 +- packages/ui/app/src/search/SearchDialog.tsx | 26 +- .../search/algolia/AlgoliaSearchDialog.tsx | 54 +- .../src/sidebar/CollapseSidebarContext.tsx | 136 +- .../ui/app/src/sidebar/DismissableSidebar.tsx | 65 + .../src/sidebar/MobileSidebarHeaderLinks.tsx | 7 +- packages/ui/app/src/sidebar/Sidebar.scss | 13 + packages/ui/app/src/sidebar/Sidebar.tsx | 129 +- .../ui/app/src/sidebar/SidebarContainer.tsx | 64 + .../src/sidebar/SidebarFixedItemsSection.tsx | 29 +- packages/ui/app/src/sidebar/SidebarLink.tsx | 21 +- .../src/sidebar/nodes/SidebarApiLeafNode.tsx | 7 +- .../sidebar/nodes/SidebarApiPackageNode.tsx | 7 +- .../sidebar/nodes/SidebarChangelogNode.tsx | 4 +- .../app/src/sidebar/nodes/SidebarPageNode.tsx | 6 +- .../nodes/SidebarRootApiPackageNode.tsx | 6 +- .../src/sidebar/nodes/SidebarRootHeading.tsx | 4 +- .../app/src/sidebar/nodes/SidebarRootNode.tsx | 6 +- .../sidebar/nodes/SidebarRootSectionNode.tsx | 6 +- .../src/sidebar/nodes/SidebarSectionNode.tsx | 7 +- packages/ui/app/src/sidebar/utils.ts | 27 + .../CodeBlockWithClipboardButton.tsx | 2 +- packages/ui/app/src/themes/ThemedDocs.scss | 2 + packages/ui/app/src/themes/ThemedDocs.tsx | 14 + .../ui/app/src/themes/cohere/CohereDocs.scss | 158 ++ .../ui/app/src/themes/cohere/CohereDocs.tsx | 92 + .../app/src/themes/cohere/HeaderContainer.tsx | 32 + .../app/src/themes/default/DefaultDocs.scss | 178 ++ .../ui/app/src/themes/default/DefaultDocs.tsx | 73 + .../src/themes/default/HeaderContainer.tsx | 69 + .../util/convertNavigatableToResolvedPath.ts | 21 +- .../ui/app/src/util/generateRadixColors.ts | 2 +- packages/ui/components/src/FernCard.scss | 2 +- packages/ui/components/src/FernDropdown.tsx | 8 +- packages/ui/components/src/FernRadioGroup.tsx | 5 +- .../components/src/FernSegmentedControl.tsx | 4 +- packages/ui/components/src/FernTabs.tsx | 2 +- packages/ui/components/src/FernTooltip.tsx | 4 +- packages/ui/docs-bundle/next.config.js | 3 +- .../src/pages/api/fern-docs/changelog.ts | 2 +- .../src/pages/api/fern-docs/feature-flags.ts | 4 + .../docs-bundle/src/utils/getDocsPageProps.ts | 1 + .../ui/local-preview-bundle/next.config.js | 2 +- .../src/utils/getDocsPageProps.ts | 1 + packages/ui/tailwind.config.js | 23 +- pnpm-lock.yaml | 1487 ++--------------- pnpm-workspace.yaml | 1 + 144 files changed, 2828 insertions(+), 2958 deletions(-) create mode 100644 packages/ui/app/src/api-page/endpoints/EndpointStreamingEnabledToggle.tsx create mode 100644 packages/ui/app/src/atoms/logo.ts create mode 100644 packages/ui/app/src/atoms/viewport.ts delete mode 100644 packages/ui/app/src/atoms/window.ts rename packages/ui/app/src/{api-page => components}/Breadcrumbs.tsx (100%) create mode 100644 packages/ui/app/src/components/EditThisPage.tsx create mode 100644 packages/ui/app/src/components/PageHeader.tsx rename packages/ui/app/src/{custom-docs-page => components}/TableOfContents.tsx (99%) delete mode 100644 packages/ui/app/src/contexts/layout-breakpoint/LayoutBreakpoint.ts delete mode 100644 packages/ui/app/src/contexts/layout-breakpoint/LayoutBreakpointProvider.tsx delete mode 100644 packages/ui/app/src/contexts/layout-breakpoint/useLayoutBreakpoint.ts create mode 100644 packages/ui/app/src/contexts/useApiPageContext.ts delete mode 100644 packages/ui/app/src/contexts/useIsReady.tsx delete mode 100644 packages/ui/app/src/docs/Docs.tsx create mode 100644 packages/ui/app/src/docs/Header.scss delete mode 100644 packages/ui/app/src/docs/HeaderContainer.tsx delete mode 100644 packages/ui/app/src/docs/HeaderLogoSection.css create mode 100644 packages/ui/app/src/docs/HeaderLogoSection.scss create mode 100644 packages/ui/app/src/docs/MobileMenuButton.tsx create mode 100644 packages/ui/app/src/layout/CustomLayout.tsx create mode 100644 packages/ui/app/src/layout/GuideLayout.tsx create mode 100644 packages/ui/app/src/layout/OverviewLayout.tsx create mode 100644 packages/ui/app/src/layout/PageLayout.tsx create mode 100644 packages/ui/app/src/layout/ReferenceLayout.tsx create mode 100644 packages/ui/app/src/mdx/frontmatter-context.tsx create mode 100644 packages/ui/app/src/mdx/frontmatter.ts create mode 100644 packages/ui/app/src/sidebar/DismissableSidebar.tsx create mode 100644 packages/ui/app/src/sidebar/Sidebar.scss create mode 100644 packages/ui/app/src/sidebar/SidebarContainer.tsx create mode 100644 packages/ui/app/src/sidebar/utils.ts create mode 100644 packages/ui/app/src/themes/ThemedDocs.scss create mode 100644 packages/ui/app/src/themes/ThemedDocs.tsx create mode 100644 packages/ui/app/src/themes/cohere/CohereDocs.scss create mode 100644 packages/ui/app/src/themes/cohere/CohereDocs.tsx create mode 100644 packages/ui/app/src/themes/cohere/HeaderContainer.tsx create mode 100644 packages/ui/app/src/themes/default/DefaultDocs.scss create mode 100644 packages/ui/app/src/themes/default/DefaultDocs.tsx create mode 100644 packages/ui/app/src/themes/default/HeaderContainer.tsx diff --git a/package.json b/package.json index eefcb2cfba..ddc1b566ea 100644 --- a/package.json +++ b/package.json @@ -90,8 +90,6 @@ }, "resolutions": { "postcss": "8.4.31", - "@mdx-js/mdx": "3.0.1", - "@mdx-js/react": "3.0.1", "esbuild": "0.20.2" }, "packageManager": "pnpm@8.15.4", diff --git a/packages/ui/app/package.json b/packages/ui/app/package.json index ecb52582c4..e476bba252 100644 --- a/packages/ui/app/package.json +++ b/packages/ui/app/package.json @@ -88,7 +88,7 @@ "mdast-util-to-hast": "^13.1.0", "moment": "^2.30.1", "next": "^14.2.3", - "next-mdx-remote": "^4.4.1", + "next-mdx-remote": "^5.0.0", "next-themes": "^0.3.0", "nprogress": "^0.2.0", "numeral": "^2.0.6", diff --git a/packages/ui/app/src/__test__/setup.ts b/packages/ui/app/src/__test__/setup.ts index 04491f43d2..ca4a32d496 100644 --- a/packages/ui/app/src/__test__/setup.ts +++ b/packages/ui/app/src/__test__/setup.ts @@ -5,7 +5,11 @@ import { afterEach, beforeAll, expect, vi } from "vitest"; expect.extend(matchers); beforeAll(() => { - vi.mock("next/router", () => require("next-router-mock")); + vi.mock("next/router", async () => { + const mock = await require("next-router-mock"); + mock.Router = mock.memoryRouter; + return mock; + }); }); afterEach(() => { diff --git a/packages/ui/app/src/api-page/ApiPage.tsx b/packages/ui/app/src/api-page/ApiPage.tsx index d379b7a5a5..036fec1c53 100644 --- a/packages/ui/app/src/api-page/ApiPage.tsx +++ b/packages/ui/app/src/api-page/ApiPage.tsx @@ -2,9 +2,10 @@ import { EMPTY_ARRAY } from "@fern-ui/core-utils"; import { useSetAtom } from "jotai"; import { useEffect } from "react"; import { APIS } from "../atoms/apis"; -import { useFeatureFlags } from "../atoms/flags"; -import { useIsReady } from "../contexts/useIsReady"; +import { useIsReady } from "../atoms/viewport"; +import { ApiPageContext } from "../contexts/useApiPageContext"; import { ResolvedRootPackage } from "../resolver/types"; +import { BuiltWithFern } from "../sidebar/BuiltWithFern"; import { ApiPackageContents } from "./ApiPackageContents"; export declare namespace ApiPage { @@ -16,7 +17,6 @@ export declare namespace ApiPage { export const ApiPage: React.FC = ({ initialApi, showErrors }) => { const hydrated = useIsReady(); - const { isApiScrollingDisabled } = useFeatureFlags(); const setDefinitions = useSetAtom(APIS); useEffect(() => { @@ -24,7 +24,7 @@ export const ApiPage: React.FC = ({ initialApi, showErrors }) => }, [initialApi, setDefinitions]); return ( -
+ = ({ initialApi, showErrors }) => anchorIdParts={EMPTY_ARRAY} /> - {isApiScrollingDisabled && ( + {/* {isApiScrollingDisabled && (
- {/* */} +
- )} + )} */} {/* anchor links should get additional padding to scroll to on initial load */} {!hydrated &&
} -
+
+ + ); }; diff --git a/packages/ui/app/src/api-page/ApiSectionMarkdownPage.tsx b/packages/ui/app/src/api-page/ApiSectionMarkdownPage.tsx index 196369f886..4057ce96a9 100644 --- a/packages/ui/app/src/api-page/ApiSectionMarkdownPage.tsx +++ b/packages/ui/app/src/api-page/ApiSectionMarkdownPage.tsx @@ -1,37 +1,41 @@ import clsx from "clsx"; -import { ReactElement } from "react"; +import { ReactElement, memo } from "react"; import { useShouldHideFromSsg } from "../contexts/navigation-context/useNavigationContext"; import { MdxContent } from "../mdx/MdxContent"; import { ResolvedPageMetadata } from "../resolver/types"; import { useApiPageCenterElement } from "./useApiPageCenterElement"; -export const ApiSectionMarkdownPage = ({ - page, - hideBottomSeparator, -}: { - page: ResolvedPageMetadata; - hideBottomSeparator: boolean; -}): ReactElement | null => { - const slug = page.slug; +export const ApiSectionMarkdownPage = memo( + ({ + page, + hideBottomSeparator, + }: { + page: ResolvedPageMetadata; + hideBottomSeparator: boolean; + }): ReactElement | null => { + const slug = page.slug; - const { setTargetRef } = useApiPageCenterElement({ slug }); + const { setTargetRef } = useApiPageCenterElement({ slug }); - // TODO: this is a temporary fix to only SSG the content that is requested by the requested route. - // - webcrawlers will accurately determine the canonical URL (right now every page "returns" the same full-length content) - // - this allows us to render the static page before hydrating, preventing layout-shift caused by the navigation context. - if (useShouldHideFromSsg(slug)) { - return null; - } + // TODO: this is a temporary fix to only SSG the content that is requested by the requested route. + // - webcrawlers will accurately determine the canonical URL (right now every page "returns" the same full-length content) + // - this allows us to render the static page before hydrating, preventing layout-shift caused by the navigation context. + if (useShouldHideFromSsg(slug)) { + return null; + } - return ( -
- -
- ); -}; + return ( +
+ +
+ ); + }, +); + +ApiSectionMarkdownPage.displayName = "ApiSectionMarkdownPage"; diff --git a/packages/ui/app/src/api-page/artifacts/ApiArtifacts.tsx b/packages/ui/app/src/api-page/artifacts/ApiArtifacts.tsx index 6bc70bf45b..76e9703b89 100644 --- a/packages/ui/app/src/api-page/artifacts/ApiArtifacts.tsx +++ b/packages/ui/app/src/api-page/artifacts/ApiArtifacts.tsx @@ -26,7 +26,7 @@ export const ApiArtifacts: React.FC = ({ apiDefinition, apiA return ( -
+

{API_ARTIFACTS_TITLE}

Official open-source client libraries for your favorite platforms. diff --git a/packages/ui/app/src/api-page/endpoints/Endpoint.tsx b/packages/ui/app/src/api-page/endpoints/Endpoint.tsx index 6b62b11482..2d77143e38 100644 --- a/packages/ui/app/src/api-page/endpoints/Endpoint.tsx +++ b/packages/ui/app/src/api-page/endpoints/Endpoint.tsx @@ -1,7 +1,9 @@ -import { useAtomValue } from "jotai"; +import { useAtom } from "jotai"; +import { memo, useEffect } from "react"; import { useFeatureFlags } from "../../atoms/flags"; +import { useResolvedPath } from "../../atoms/navigation"; import { FERN_STREAM_ATOM } from "../../atoms/stream"; -import { useNavigationContext, useShouldHideFromSsg } from "../../contexts/navigation-context/useNavigationContext"; +import { useShouldHideFromSsg } from "../../contexts/navigation-context/useNavigationContext"; import { ResolvedEndpointDefinition, ResolvedTypeDefinition } from "../../resolver/types"; import { useApiPageCenterElement } from "../useApiPageCenterElement"; import { EndpointContent } from "./EndpointContent"; @@ -17,13 +19,30 @@ export declare namespace Endpoint { } } -export const Endpoint: React.FC = ({ api, showErrors, endpoint, breadcrumbs, isLastInApi, types }) => { - const isStream = useAtomValue(FERN_STREAM_ATOM); - const { resolvedPath } = useNavigationContext(); +const UnmemoizedEndpoint: React.FC = ({ + api, + showErrors, + endpoint, + breadcrumbs, + isLastInApi, + types, +}) => { + const [isStream, setStream] = useAtom(FERN_STREAM_ATOM); + const resolvedPath = useResolvedPath(); const { isApiScrollingDisabled } = useFeatureFlags(); const endpointSlug = endpoint.stream != null && isStream ? endpoint.stream.slug : endpoint.slug; + useEffect(() => { + if (endpoint.stream != null) { + if (endpoint.slug === resolvedPath.fullSlug) { + setStream(false); + } else if (endpoint.stream.slug === resolvedPath.fullSlug) { + setStream(true); + } + } + }, [endpoint.slug, endpoint.stream, resolvedPath.fullSlug, setStream]); + const { setTargetRef } = useApiPageCenterElement({ slug: endpointSlug }); // TODO: this is a temporary fix to only SSG the content that is requested by the requested route. @@ -41,8 +60,9 @@ export const Endpoint: React.FC = ({ api, showErrors, endpoint, breadcrumbs={breadcrumbs} containerRef={setTargetRef} hideBottomSeparator={isLastInApi || isApiScrollingDisabled} - isInViewport={resolvedPath.fullSlug === endpoint.slug} types={types} /> ); }; + +export const Endpoint = memo(UnmemoizedEndpoint); diff --git a/packages/ui/app/src/api-page/endpoints/EndpointContent.tsx b/packages/ui/app/src/api-page/endpoints/EndpointContent.tsx index bb780ac19d..a903db42bd 100644 --- a/packages/ui/app/src/api-page/endpoints/EndpointContent.tsx +++ b/packages/ui/app/src/api-page/endpoints/EndpointContent.tsx @@ -1,25 +1,29 @@ -import { visitDiscriminatedUnion } from "@fern-ui/core-utils"; import cn from "clsx"; -import { useAtom } from "jotai"; +import { atom, useAtom, useAtomValue } from "jotai"; +import { selectAtom } from "jotai/utils"; +import { isEqual } from "lodash-es"; import dynamic from "next/dynamic"; -import { useRouter } from "next/router"; -import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"; import { useInView } from "react-intersection-observer"; +import { useCallbackOne } from "use-memo-one"; import { FERN_LANGUAGE_ATOM } from "../../atoms/lang"; +import { CONTENT_HEIGHT_ATOM } from "../../atoms/layout"; +import { HASH_ATOM } from "../../atoms/location"; +import { CURRENT_NODE_ID_ATOM } from "../../atoms/navigation"; +import { store } from "../../atoms/store"; import { FERN_STREAM_ATOM } from "../../atoms/stream"; -import { useDocsContext } from "../../contexts/docs-context/useDocsContext"; -import { useLayoutBreakpoint } from "../../contexts/layout-breakpoint/useLayoutBreakpoint"; -import { useViewportSize } from "../../hooks/useViewportSize"; +import { BREAKPOINT_ATOM, MOBILE_SIDEBAR_ENABLED_ATOM } from "../../atoms/viewport"; +import { Breadcrumbs } from "../../components/Breadcrumbs"; +import { useAtomEffect } from "../../hooks/useAtomEffect"; import { ResolvedEndpointDefinition, ResolvedError, ResolvedTypeDefinition } from "../../resolver/types"; import { ApiPageDescription } from "../ApiPageDescription"; -import { Breadcrumbs } from "../Breadcrumbs"; import { JsonPropertyPath } from "../examples/JsonPropertyPath"; import { CodeExample, generateCodeExamples } from "../examples/code-example"; import { AnimatedTitle } from "./AnimatedTitle"; import { EndpointAvailabilityTag } from "./EndpointAvailabilityTag"; import { EndpointContentLeft, convertNameToAnchorPart } from "./EndpointContentLeft"; +import { EndpointStreamingEnabledToggle } from "./EndpointStreamingEnabledToggle"; import { EndpointUrlWithOverflow } from "./EndpointUrlWithOverflow"; -import { StreamingEnabledToggle } from "./StreamingEnabledToggle"; const EndpointContentCodeSnippets = dynamic( () => import("./EndpointContentCodeSnippets").then((mod) => mod.EndpointContentCodeSnippets), @@ -34,7 +38,6 @@ export declare namespace EndpointContent { breadcrumbs: readonly string[]; hideBottomSeparator?: boolean; containerRef: React.Ref; - isInViewport: boolean; types: Record; } } @@ -66,14 +69,15 @@ function maybeGetErrorStatusCodeOrNameFromAnchor(anchor: string | undefined): nu return undefined; } -export const EndpointContent: React.FC = ({ +const paddingAtom = atom((get) => (get(MOBILE_SIDEBAR_ENABLED_ATOM) ? 0 : 26)); + +const UnmemoizedEndpointContent: React.FC = ({ api, showErrors, endpoint: endpointProp, breadcrumbs, hideBottomSeparator = false, containerRef, - isInViewport: initiallyInViewport, types, }) => { const [isStream, setIsStream] = useAtom(FERN_STREAM_ATOM); @@ -83,15 +87,13 @@ export const EndpointContent: React.FC = ({ useImperativeHandle(containerRef, () => ref.current); - const router = useRouter(); - const { layout } = useDocsContext(); - const layoutBreakpoint = useLayoutBreakpoint(); - const viewportSize = useViewportSize(); - const [isInViewport, setIsInViewport] = useState(initiallyInViewport); + const [isInViewport, setIsInViewport] = useState(() => store.get(CURRENT_NODE_ID_ATOM) === endpoint.nodeId); const { ref: viewportRef } = useInView({ onChange: setIsInViewport, rootMargin: "100%", }); + useImperativeHandle(viewportRef, () => ref.current ?? undefined); + const [hoveredRequestPropertyPath, setHoveredRequestPropertyPath] = useState(); const [hoveredResponsePropertyPath, setHoveredResponsePropertyPath] = useState(); const onHoverRequestProperty = useCallback( @@ -109,20 +111,23 @@ export const EndpointContent: React.FC = ({ const [selectedError, setSelectedError] = useState(); - useEffect(() => { - const statusCodeOrName = maybeGetErrorStatusCodeOrNameFromAnchor(router.asPath.split("#")[1]); - if (statusCodeOrName != null) { - const error = endpoint.errors.find((e) => - typeof statusCodeOrName === "number" - ? e.statusCode === statusCodeOrName - : convertNameToAnchorPart(e.name) === statusCodeOrName, - ); - if (error != null) { - setSelectedError(error); + useAtomEffect( + useCallbackOne((get) => { + const hash = get(HASH_ATOM); + const statusCodeOrName = maybeGetErrorStatusCodeOrNameFromAnchor(hash); + if (statusCodeOrName != null) { + const error = endpoint.errors.find((e) => + typeof statusCodeOrName === "number" + ? e.statusCode === statusCodeOrName + : convertNameToAnchorPart(e.name) === statusCodeOrName, + ); + if (error != null) { + setSelectedError(error); + } } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []), + ); const examples = useMemo(() => { if (selectedError == null) { @@ -165,59 +170,78 @@ export const EndpointContent: React.FC = ({ const selectorHeight = (clients.find((c) => c.language === selectedClient.language)?.examples.length ?? 0) > 1 ? GAP_6 + 24 : 0; - const headerHeight = - layout?.headerHeight == null - ? 64 - : visitDiscriminatedUnion(layout.headerHeight, "type")._visit({ - px: (px) => px.value, - rem: (rem) => rem.value * 16, - _other: () => 64, - }); - const jsonLineLength = responseCodeSnippet?.split("\n").length ?? 0; - const [requestHeight, responseHeight] = useMemo((): [number, number] => { - if (layoutBreakpoint.max("lg")) { - const requestLines = Math.min(MOBILE_MAX_LINES + 1, selectedExampleClientLineCount); - const responseLines = Math.min(MOBILE_MAX_LINES + 1, jsonLineLength); - const requestContainerHeight = requestLines * LINE_HEIGHT + CONTENT_PADDING; - const responseContainerHeight = responseLines * LINE_HEIGHT + CONTENT_PADDING; - return [requestContainerHeight, responseContainerHeight]; - } - const maxRequestContainerHeight = selectedExampleClientLineCount * LINE_HEIGHT + CONTENT_PADDING; - const maxResponseContainerHeight = jsonLineLength * LINE_HEIGHT + CONTENT_PADDING; - const containerHeight = viewportSize.height - headerHeight - PADDING_TOP - PADDING_BOTTOM - selectorHeight; - const halfContainerHeight = (containerHeight - GAP_6) / 2; - if (selectedClient.exampleCall?.responseBody == null) { - return [Math.min(maxRequestContainerHeight, containerHeight), 0]; - } - if (maxRequestContainerHeight >= halfContainerHeight && maxResponseContainerHeight >= halfContainerHeight) { - return [halfContainerHeight, halfContainerHeight]; - } else if (maxRequestContainerHeight + maxResponseContainerHeight <= containerHeight - GAP_6) { - return [maxRequestContainerHeight, maxResponseContainerHeight]; - } else if (maxRequestContainerHeight < halfContainerHeight) { - const remainingContainerHeight = containerHeight - maxRequestContainerHeight - GAP_6; - return [maxRequestContainerHeight, Math.min(remainingContainerHeight, maxResponseContainerHeight)]; - } else if (maxResponseContainerHeight < halfContainerHeight) { - const remainingContainerHeight = containerHeight - maxResponseContainerHeight - GAP_6; - return [Math.min(remainingContainerHeight, maxRequestContainerHeight), maxResponseContainerHeight]; - } else { - return [0, 0]; - } - }, [ - layoutBreakpoint, - selectedExampleClientLineCount, - jsonLineLength, - viewportSize.height, - headerHeight, - selectorHeight, - selectedClient.exampleCall?.responseBody, - ]); + const [requestHeight, responseHeight] = useAtomValue( + useMemo( + () => + selectAtom( + atom((get): [number, number] => { + const isMobileSidebarEnabled = get(MOBILE_SIDEBAR_ENABLED_ATOM); + const contentHeight = get(CONTENT_HEIGHT_ATOM); + if (isMobileSidebarEnabled) { + const requestLines = Math.min(MOBILE_MAX_LINES + 1, selectedExampleClientLineCount); + const responseLines = Math.min(MOBILE_MAX_LINES + 1, jsonLineLength); + const requestContainerHeight = requestLines * LINE_HEIGHT + CONTENT_PADDING; + const responseContainerHeight = responseLines * LINE_HEIGHT + CONTENT_PADDING; + return [requestContainerHeight, responseContainerHeight]; + } + const maxRequestContainerHeight = + selectedExampleClientLineCount * LINE_HEIGHT + CONTENT_PADDING; + const maxResponseContainerHeight = jsonLineLength * LINE_HEIGHT + CONTENT_PADDING; + const containerHeight = contentHeight - PADDING_TOP - PADDING_BOTTOM - selectorHeight; + const halfContainerHeight = (containerHeight - GAP_6) / 2; + if (selectedClient.exampleCall?.responseBody == null) { + return [Math.min(maxRequestContainerHeight, containerHeight), 0]; + } + if ( + maxRequestContainerHeight >= halfContainerHeight && + maxResponseContainerHeight >= halfContainerHeight + ) { + return [halfContainerHeight, halfContainerHeight]; + } else if (maxRequestContainerHeight + maxResponseContainerHeight <= containerHeight - GAP_6) { + return [maxRequestContainerHeight, maxResponseContainerHeight]; + } else if (maxRequestContainerHeight < halfContainerHeight) { + const remainingContainerHeight = containerHeight - maxRequestContainerHeight - GAP_6; + return [ + maxRequestContainerHeight, + Math.min(remainingContainerHeight, maxResponseContainerHeight), + ]; + } else if (maxResponseContainerHeight < halfContainerHeight) { + const remainingContainerHeight = containerHeight - maxResponseContainerHeight - GAP_6; + return [ + Math.min(remainingContainerHeight, maxRequestContainerHeight), + maxResponseContainerHeight, + ]; + } else { + return [0, 0]; + } + }), + (v) => v, + isEqual, + ), + [jsonLineLength, selectedClient.exampleCall?.responseBody, selectedExampleClientLineCount, selectorHeight], + ), + ); - const padding = layoutBreakpoint.max("lg") ? 0 : 26; + const padding = useAtomValue(paddingAtom); const initialExampleHeight = requestHeight + responseHeight + (responseHeight > 0 && requestHeight > 0 ? GAP_6 : 0) + padding; const [exampleHeight, setExampleHeight] = useState(initialExampleHeight); + const minHeight = useAtomValue( + useMemo( + () => + atom((get) => { + const breakpoint = get(BREAKPOINT_ATOM); + if (breakpoint === "sm" || breakpoint === "mobile") { + return 0; + } else { + return exampleHeight + 8 * 2 * 4; + } + }), + [exampleHeight], + ), + ); // Reset the example height (not in view) if the viewport height changes useEffect(() => { @@ -229,22 +253,21 @@ export const EndpointContent: React.FC = ({ return (
setSelectedError(undefined)} - ref={viewportRef} + ref={ref} + data-route={`/${endpoint.slug}`} >
-

+

{endpoint.title}

{endpoint.availability != null && ( @@ -255,22 +278,11 @@ export const EndpointContent: React.FC = ({
{endpointProp.stream != null && ( - { - setIsStream(value); - const endpoint = - value && endpointProp.stream != null ? endpointProp.stream : endpointProp; - void router.replace(`/${endpoint.slug}`, undefined, { - shallow: true, - }); - setTimeout(() => { - if (ref.current != null) { - ref.current.scrollIntoView({ behavior: "instant" }); - } - }, 0); - }} + setValue={setIsStream} + endpointProp={endpointProp} + container={ref} /> )}
@@ -287,7 +299,7 @@ export const EndpointContent: React.FC = ({ className="flex min-w-0 max-w-content-width flex-1 flex-col pt-8 md:py-8" style={{ // TODO: do we still need to set minHeight here? - minHeight: layoutBreakpoint.min("md") ? `${exampleHeight}px` : undefined, + minHeight: `${minHeight}px`, }} > = ({ />
-
+
+
); }; + +export const EndpointContent = memo(UnmemoizedEndpointContent); diff --git a/packages/ui/app/src/api-page/endpoints/EndpointContentCodeSnippets.tsx b/packages/ui/app/src/api-page/endpoints/EndpointContentCodeSnippets.tsx index 8655975b60..e8d9be559d 100644 --- a/packages/ui/app/src/api-page/endpoints/EndpointContentCodeSnippets.tsx +++ b/packages/ui/app/src/api-page/endpoints/EndpointContentCodeSnippets.tsx @@ -125,10 +125,7 @@ const UnmemoizedEndpointContentCodeSnippets: React.FC +
{/* TODO: Replace this with a proper segmented control component */} {selectedClientGroup != null && selectedClientGroup.examples.length > 1 && ( diff --git a/packages/ui/app/src/api-page/endpoints/EndpointParameter.tsx b/packages/ui/app/src/api-page/endpoints/EndpointParameter.tsx index 1c27f10f87..c000781936 100644 --- a/packages/ui/app/src/api-page/endpoints/EndpointParameter.tsx +++ b/packages/ui/app/src/api-page/endpoints/EndpointParameter.tsx @@ -93,7 +93,7 @@ export const EndpointParameterContent: FC = ({ const anchorRoute = `${route}#${anchorId}`; return ( -
+
{createElement( headerType, diff --git a/packages/ui/app/src/api-page/endpoints/EndpointStreamingEnabledToggle.tsx b/packages/ui/app/src/api-page/endpoints/EndpointStreamingEnabledToggle.tsx new file mode 100644 index 0000000000..c275fcdcc4 --- /dev/null +++ b/packages/ui/app/src/api-page/endpoints/EndpointStreamingEnabledToggle.tsx @@ -0,0 +1,36 @@ +import { useRouter } from "next/router"; +import { MutableRefObject, ReactElement } from "react"; +import { ResolvedEndpointDefinition } from "../../resolver/types"; +import { StreamingEnabledToggle } from "./StreamingEnabledToggle"; + +export function EndpointStreamingEnabledToggle({ + value, + setValue, + endpointProp, + container, +}: { + value: boolean; + setValue: (enabled: boolean) => void; + endpointProp: ResolvedEndpointDefinition; + container: MutableRefObject; +}): ReactElement { + const router = useRouter(); + return ( + { + setValue(value); + const endpoint = value && endpointProp.stream != null ? endpointProp.stream : endpointProp; + void router.replace(`/${endpoint.slug}`, undefined, { + shallow: true, + }); + setTimeout(() => { + if (container.current != null) { + container.current.scrollIntoView({ behavior: "instant" }); + } + }, 0); + }} + /> + ); +} diff --git a/packages/ui/app/src/api-page/endpoints/ErrorExampleSelect.tsx b/packages/ui/app/src/api-page/endpoints/ErrorExampleSelect.tsx index d4e1a54025..cff7a58bb8 100644 --- a/packages/ui/app/src/api-page/endpoints/ErrorExampleSelect.tsx +++ b/packages/ui/app/src/api-page/endpoints/ErrorExampleSelect.tsx @@ -80,7 +80,7 @@ export const ErrorExampleSelect: FC> - + @@ -122,7 +122,7 @@ export const ErrorExampleSelect: FC> ))} - + diff --git a/packages/ui/app/src/api-page/examples/TitledExample.tsx b/packages/ui/app/src/api-page/examples/TitledExample.tsx index 2d96069505..d459fe33c9 100644 --- a/packages/ui/app/src/api-page/examples/TitledExample.tsx +++ b/packages/ui/app/src/api-page/examples/TitledExample.tsx @@ -24,8 +24,8 @@ export const TitledExample = forwardRef -
+
{typeof title === "string" ? (
= ({ const { setTargetRef } = useApiPageCenterElement({ slug: apiDefinition.slug }); return ( <> - -
- +
- + { + const onChangeIsInVerticalCenter = useEventCallback((newIsInVerticalCenter: boolean) => { if (newIsInVerticalCenter) { onScrollToPath(slug); } @@ -28,7 +31,7 @@ export function useApiPageCenterElement({ slug }: useApiPageCenterElement.Args): rootMargin: "-50% 0px", threshold: 0, initialInView: isSelected, - onChange: onChangeIsInVerticalCenter.current, + onChange: onChangeIsInVerticalCenter, }); return { diff --git a/packages/ui/app/src/api-page/web-socket/WebSocket.tsx b/packages/ui/app/src/api-page/web-socket/WebSocket.tsx index 24ce1f2c8a..93e4ab7ee1 100644 --- a/packages/ui/app/src/api-page/web-socket/WebSocket.tsx +++ b/packages/ui/app/src/api-page/web-socket/WebSocket.tsx @@ -105,13 +105,9 @@ const WebhookContent: FC = ({ websocket, isLastInApi, types }) const headers = websocket.headers.filter((header) => !header.hidden); return ( -
+
@@ -310,7 +306,7 @@ const WebhookContent: FC = ({ websocket, isLastInApi, types })
+ +
); diff --git a/packages/ui/app/src/docs/Docs.tsx b/packages/ui/app/src/docs/Docs.tsx deleted file mode 100644 index 31d0ff3db6..0000000000 --- a/packages/ui/app/src/docs/Docs.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { DocsV1Read } from "@fern-api/fdr-sdk"; -import clsx from "clsx"; -import dynamic from "next/dynamic"; -import { memo } from "react"; -import { PlaygroundContextProvider } from "../api-playground/PlaygroundContext"; -import { useFeatureFlags } from "../atoms/flags"; -import { useSidebarNodes } from "../atoms/navigation"; -import { useIsMobileSidebarOpen, useMessageHandler } from "../atoms/sidebar"; -import { useDocsContext } from "../contexts/docs-context/useDocsContext"; -import { useLayoutBreakpointValue } from "../contexts/layout-breakpoint/useLayoutBreakpoint"; -import { useNavigationContext } from "../contexts/navigation-context"; -import { FeedbackPopover } from "../custom-docs-page/FeedbackPopover"; -import { BuiltWithFern } from "../sidebar/BuiltWithFern"; -import { DocsMainContent } from "./DocsMainContent"; -import { HeaderContainer } from "./HeaderContainer"; - -const Sidebar = dynamic(() => import("../sidebar/Sidebar").then(({ Sidebar }) => Sidebar), { ssr: true }); - -interface DocsProps { - logoHeight: DocsV1Read.Height | undefined; - logoHref: DocsV1Read.Url | undefined; -} - -export const SearchDialog = dynamic(() => import("../search/SearchDialog").then(({ SearchDialog }) => SearchDialog), { - ssr: true, -}); - -export const Docs: React.FC = memo(function UnmemoizedDocs({ logoHeight, logoHref }) { - const { layout, colors } = useDocsContext(); - const sidebar = useSidebarNodes(); - const { isInlineFeedbackEnabled } = useFeatureFlags(); - const { resolvedPath } = useNavigationContext(); - - // set up message handler to listen for messages from custom scripts - useMessageHandler(); - - const isMobileSidebarOpen = useIsMobileSidebarOpen(); - const layoutBreakpoint = useLayoutBreakpointValue(); - - const docsMainContent = ; - - return ( - -
- {(layout?.disableHeader !== true || ["mobile", "sm", "md"].includes(layoutBreakpoint)) && ( - - )} - -
- {sidebar != null && resolvedPath.type !== "changelog-entry" && ( - <> - -
- - ); -}); diff --git a/packages/ui/app/src/docs/DocsMainContent.tsx b/packages/ui/app/src/docs/DocsMainContent.tsx index 893eaa21fb..ff246af131 100644 --- a/packages/ui/app/src/docs/DocsMainContent.tsx +++ b/packages/ui/app/src/docs/DocsMainContent.tsx @@ -1,14 +1,17 @@ +import { visitDiscriminatedUnion } from "@fern-ui/core-utils"; import dynamic from "next/dynamic"; import { useRouter } from "next/router"; -import { useNavigationContext } from "../contexts/navigation-context"; -import { useIsReady } from "../contexts/useIsReady"; +import { Fragment, ReactElement, memo } from "react"; +import { useFeatureFlags } from "../atoms/flags"; +import { useResolvedPath } from "../atoms/navigation"; +import { useIsReady } from "../atoms/viewport"; +import { FernErrorBoundary } from "../components/FernErrorBoundary"; +import { FeedbackPopover } from "../custom-docs-page/FeedbackPopover"; import { ChangelogEntryPage } from "./ChangelogEntryPage"; const CustomDocsPage = dynamic( () => import("../custom-docs-page/CustomDocsPage").then(({ CustomDocsPage }) => CustomDocsPage), - { - ssr: true, - }, + { ssr: true }, ); const ApiPage = dynamic(() => import("../api-page/ApiPage").then(({ ApiPage }) => ApiPage), { @@ -21,8 +24,8 @@ const ChangelogPage = dynamic(() => import("./ChangelogPage").then(({ ChangelogP export interface DocsMainContentProps {} -export const DocsMainContent: React.FC = () => { - const { resolvedPath } = useNavigationContext(); +function DocsMainContentInternal(): ReactElement | null { + const resolvedPath = useResolvedPath(); const hydrated = useIsReady(); const router = useRouter(); @@ -32,15 +35,27 @@ export const DocsMainContent: React.FC = () => { } } - if (resolvedPath.type === "custom-markdown-page") { - return ; - } else if (resolvedPath.type === "api-page") { - return ; - } else if (resolvedPath.type === "changelog") { - return ; - } else if (resolvedPath.type === "changelog-entry") { - return ; - } else { - return null; - } -}; + return visitDiscriminatedUnion(resolvedPath)._visit({ + "custom-markdown-page": (resolvedPath) => ( + + ), + "api-page": (resolvedPath) => ( + + ), + changelog: (resolvedPath) => , + "changelog-entry": (resolvedPath) => , + _other: () => null, + }); +} + +export const DocsMainContent = memo(function DocsMainContent(): ReactElement { + const { isInlineFeedbackEnabled } = useFeatureFlags(); + const FeedbackPopoverProvider = isInlineFeedbackEnabled ? FeedbackPopover : Fragment; + return ( + + + + + + ); +}); diff --git a/packages/ui/app/src/docs/Header.scss b/packages/ui/app/src/docs/Header.scss new file mode 100644 index 0000000000..03592931a4 --- /dev/null +++ b/packages/ui/app/src/docs/Header.scss @@ -0,0 +1,17 @@ +@layer components { + .fern-header-searchbar { + @apply max-w-content-width w-full max-lg:hidden shrink min-w-0 mx-2; + } + + .fern-header-right-menu { + @apply -mr-1 flex items-center justify-end space-x-0 md:mr-0 lg:space-x-4; + + .lg-menu { + @apply hidden lg:block; + } + + .max-lg-menu { + @apply flex items-center lg:hidden; + } + } +} diff --git a/packages/ui/app/src/docs/Header.tsx b/packages/ui/app/src/docs/Header.tsx index 92f31128bc..c1e3f76479 100644 --- a/packages/ui/app/src/docs/Header.tsx +++ b/packages/ui/app/src/docs/Header.tsx @@ -1,8 +1,9 @@ import { DocsV1Read } from "@fern-api/fdr-sdk"; import { FernButton, FernButtonGroup } from "@fern-ui/components"; -import { ArrowRightIcon, Cross1Icon, HamburgerMenuIcon, MagnifyingGlassIcon } from "@radix-ui/react-icons"; +import { ArrowRightIcon, MagnifyingGlassIcon } from "@radix-ui/react-icons"; import cn from "clsx"; import { useAtomValue } from "jotai"; +import { isEqual } from "lodash-es"; import { CSSProperties, PropsWithChildren, forwardRef, memo } from "react"; import { useOpenSearchDialog } from "../atoms/sidebar"; import { FernLinkButton } from "../components/FernLinkButton"; @@ -13,43 +14,29 @@ import { SidebarSearchBar } from "../sidebar/SidebarSearchBar"; import { getGitHubRepo } from "../util/github"; import { GitHubWidget } from "./GitHubWidget"; import { HeaderLogoSection } from "./HeaderLogoSection"; +import { MobileMenuButton } from "./MobileMenuButton"; import { ThemeButton } from "./ThemeButton"; export declare namespace Header { export interface Props { className?: string; style?: CSSProperties; - navbarLinks: DocsV1Read.NavbarLink[]; - logoHeight: DocsV1Read.Height | undefined; - logoHref: DocsV1Read.Url | undefined; - isMobileSidebarOpen: boolean; - openMobileSidebar: () => void; - closeMobileSidebar: () => void; showSearchBar?: boolean; } } const UnmemoizedHeader = forwardRef>(function Header( - { - className, - style, - navbarLinks, - isMobileSidebarOpen, - openMobileSidebar, - closeMobileSidebar, - showSearchBar = true, - logoHeight, - logoHref, - }, + { className, style, showSearchBar = true }, ref, ) { + const { navbarLinks } = useDocsContext(); const { colors } = useDocsContext(); const openSearchDialog = useOpenSearchDialog(); const isSearchBoxMounted = useAtomValue(SEARCH_BOX_MOUNTED); const [searchService] = useSearchConfig(); const navbarLinksSection = ( -
+
{navbarLinks.map((navbarLink, idx) => { if (navbarLink.type === "github") { @@ -94,17 +81,12 @@ const UnmemoizedHeader = forwardRef - + ); }); -export const Header = memo(UnmemoizedHeader); +export const Header = memo( + UnmemoizedHeader, + (prev, next) => + prev.className === next.className && + isEqual(prev.style, next.style) && + prev.showSearchBar === next.showSearchBar, +); export declare namespace HeaderPrimaryLink { export interface Props { diff --git a/packages/ui/app/src/docs/HeaderContainer.tsx b/packages/ui/app/src/docs/HeaderContainer.tsx deleted file mode 100644 index da2c918f46..0000000000 --- a/packages/ui/app/src/docs/HeaderContainer.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { DocsV1Read } from "@fern-api/fdr-sdk"; -import cn from "clsx"; -import { FC, useCallback } from "react"; -import { useCloseMobileSidebar, useOpenMobileSidebar } from "../atoms/sidebar"; -import { useDocsContext } from "../contexts/docs-context/useDocsContext"; -import { useLayoutBreakpointValue } from "../contexts/layout-breakpoint/useLayoutBreakpoint"; -import { BgImageGradient } from "./BgImageGradient"; -import { Header } from "./Header"; -import { HeaderTabs } from "./HeaderTabs"; -import { useIsScrolled } from "./useIsScrolled"; - -interface HeaderContainerProps { - isMobileSidebarOpen: boolean; - logoHeight: DocsV1Read.Height | undefined; - logoHref: DocsV1Read.Url | undefined; -} - -export const HeaderContainer: FC = ({ isMobileSidebarOpen, logoHeight, logoHref }) => { - const { colors, layout, tabs, navbarLinks } = useDocsContext(); - const isScrolled = useIsScrolled(); - const layoutBreakpoint = useLayoutBreakpointValue(); - const openMobileSidebar = useOpenMobileSidebar(); - const closeMobileSidebar = useCloseMobileSidebar(); - - const renderBackground = useCallback( - (className?: string) => ( - <> - -
- -
- - ), - [colors], - ); - - return ( -
-
-
-
- {renderBackground()} -
-
- {tabs.length > 0 && layout?.tabsPlacement === "HEADER" && layout?.disableHeader !== true && ( - - )} -
-
-
- ); -}; diff --git a/packages/ui/app/src/docs/HeaderLogoSection.css b/packages/ui/app/src/docs/HeaderLogoSection.css deleted file mode 100644 index 8938d9e071..0000000000 --- a/packages/ui/app/src/docs/HeaderLogoSection.css +++ /dev/null @@ -1,3 +0,0 @@ -.fern-logo-container { - @apply flex shrink-0 items-center; -} diff --git a/packages/ui/app/src/docs/HeaderLogoSection.scss b/packages/ui/app/src/docs/HeaderLogoSection.scss new file mode 100644 index 0000000000..fd4624459c --- /dev/null +++ b/packages/ui/app/src/docs/HeaderLogoSection.scss @@ -0,0 +1,3 @@ +.fern-logo-container { + @apply flex shrink-0 items-baseline; +} diff --git a/packages/ui/app/src/docs/HeaderLogoSection.tsx b/packages/ui/app/src/docs/HeaderLogoSection.tsx index 2fb4e4e37d..7a07726f6b 100644 --- a/packages/ui/app/src/docs/HeaderLogoSection.tsx +++ b/packages/ui/app/src/docs/HeaderLogoSection.tsx @@ -1,85 +1,28 @@ -import { DocsV1Read } from "@fern-api/fdr-sdk"; import cn from "clsx"; -import Link from "next/link"; +import { useAtomValue } from "jotai"; +import { PropsWithChildren, ReactElement } from "react"; +import { LOGO_TEXT_ATOM } from "../atoms/logo"; import { FernImage } from "../components/FernImage"; +import { FernLink } from "../components/FernLink"; import { DEFAULT_LOGO_HEIGHT } from "../config"; import { useDocsContext } from "../contexts/docs-context/useDocsContext"; import { VersionDropdown } from "./VersionDropdown"; -export interface HeaderLogoSectionProps { - logoHeight: DocsV1Read.Height | undefined; - logoHref: DocsV1Read.Url | undefined; -} - -export const HeaderLogoSection: React.FC = ({ logoHeight, logoHref }) => { - const { colors, resolveFile, versions } = useDocsContext(); - const logoImageHeight = logoHeight ?? DEFAULT_LOGO_HEIGHT; - - const imageClassName = "max-h-full object-contain"; - - const renderLogoContent = () => { - if (colors == null) { - return null; - } - - if (colors.dark != null && colors.light != null) { - return ( - <> - {colors.light.logo != null && ( - - )} - {colors.dark.logo != null && ( - - )} - - ); - } else { - const logoFile = colors.light?.logo ?? colors.dark?.logo; - - if (logoFile == null) { - return null; - } - - return ( - - ); - } - }; +export function HeaderLogoSection(): ReactElement { + const { versions, logoHref } = useDocsContext(); + const logoText = useAtomValue(LOGO_TEXT_ATOM); return (
- {logoHref != null ? ( - - {renderLogoContent()} - - ) : ( -
{renderLogoContent()}
- )} + + + {logoText != null && logoText.length > 0 && ( + + {logoText} + + )} + {versions.length > 1 && (
@@ -88,4 +31,65 @@ export const HeaderLogoSection: React.FC = ({ logoHeight
); -}; +} + +function FernLogoImage(): ReactElement | null { + const { colors, resolveFile, logoHeight } = useDocsContext(); + if (colors == null) { + return null; + } + + const logoImageHeight = logoHeight ?? DEFAULT_LOGO_HEIGHT; + const imageClassName = "max-h-full object-contain"; + if (colors.dark != null && colors.light != null) { + return ( + <> + {colors.light.logo != null && ( + + )} + {colors.dark.logo != null && ( + + )} + + ); + } else { + const logoFile = colors.light?.logo ?? colors.dark?.logo; + + if (logoFile == null) { + return null; + } + + return ( + + ); + } +} + +function FernLogoContainer({ children, href }: PropsWithChildren<{ href: string | undefined }>): ReactElement { + const container =
{children}
; + return href != null ? {container} : container; +} diff --git a/packages/ui/app/src/docs/HeaderTabs.tsx b/packages/ui/app/src/docs/HeaderTabs.tsx index 633eb420b1..0b4ee0f8a2 100644 --- a/packages/ui/app/src/docs/HeaderTabs.tsx +++ b/packages/ui/app/src/docs/HeaderTabs.tsx @@ -7,28 +7,21 @@ import { getSidebarTabHref } from "../util/getSidebarTabHref"; export function HeaderTabs(): ReactElement { const { tabs, currentTabIndex } = useDocsContext(); return ( - +
    + {tabs.map((tab) => ( +
  • + +
    + {tab.icon && } + {tab.title} +
    +
    +
  • + ))} +
); } diff --git a/packages/ui/app/src/docs/MobileMenuButton.tsx b/packages/ui/app/src/docs/MobileMenuButton.tsx new file mode 100644 index 0000000000..83aa5fe19e --- /dev/null +++ b/packages/ui/app/src/docs/MobileMenuButton.tsx @@ -0,0 +1,28 @@ +import { FernButton } from "@fern-ui/components"; +import { Cross1Icon, HamburgerMenuIcon } from "@radix-ui/react-icons"; +import { ReactElement } from "react"; +import { useCloseMobileSidebar, useIsMobileSidebarOpen, useOpenMobileSidebar } from "../atoms/sidebar"; + +export function MobileMenuButton(): ReactElement { + const isMobileSidebarOpen = useIsMobileSidebarOpen(); + const closeMobileSidebar = useCloseMobileSidebar(); + const openMobileSidebar = useOpenMobileSidebar(); + return ( + { + e.stopPropagation(); + e.preventDefault(); + if (isMobileSidebarOpen) { + closeMobileSidebar(); + } else { + openMobileSidebar(); + } + }} + icon={isMobileSidebarOpen ? : } + intent={isMobileSidebarOpen ? "primary" : "none"} + variant={isMobileSidebarOpen ? "filled" : "minimal"} + rounded={true} + size="large" + /> + ); +} diff --git a/packages/ui/app/src/docs/useIsScrolled.ts b/packages/ui/app/src/docs/useIsScrolled.ts index b7d1be43b9..3e482f7327 100644 --- a/packages/ui/app/src/docs/useIsScrolled.ts +++ b/packages/ui/app/src/docs/useIsScrolled.ts @@ -1,6 +1,6 @@ import { RefObject, useCallback, useEffect, useState } from "react"; -export function useIsScrolled(ref?: RefObject): boolean { +export function useIsScrolled(ref?: RefObject): boolean { const [isScrolled, setIsScrolled] = useState(false); const getScrollY = useCallback(() => { diff --git a/packages/ui/app/src/index.ts b/packages/ui/app/src/index.ts index a12581f68b..611eab21e9 100644 --- a/packages/ui/app/src/index.ts +++ b/packages/ui/app/src/index.ts @@ -5,10 +5,11 @@ export { DEFAULT_FEATURE_FLAGS } from "./atoms/flags"; export type { FeatureFlags } from "./atoms/flags"; export { LocalPreviewContextProvider } from "./contexts/LocalPreviewContext"; export { useSetThemeColors } from "./docs/ThemeProvider"; +export { getFrontmatter } from "./mdx/frontmatter"; export * from "./next-app/DocsPage"; export { NextApp } from "./next-app/NextApp"; export { getBreadcrumbList } from "./next-app/utils/getBreadcrumbList"; -export { getDefaultSeoProps, getFrontmatter } from "./next-app/utils/getSeoProp"; +export { getDefaultSeoProps } from "./next-app/utils/getSeoProp"; export { ApiDefinitionResolver } from "./resolver/ApiDefinitionResolver"; export { ApiTypeResolver } from "./resolver/ApiTypeResolver"; export * from "./resolver/types"; diff --git a/packages/ui/app/src/layout/CustomLayout.tsx b/packages/ui/app/src/layout/CustomLayout.tsx new file mode 100644 index 0000000000..23a135b4a0 --- /dev/null +++ b/packages/ui/app/src/layout/CustomLayout.tsx @@ -0,0 +1,20 @@ +import type { ElementContent } from "hast"; +import { MdxJsxFlowElementHast } from "mdast-util-mdx-jsx"; +import { ReactElement, ReactNode } from "react"; + +interface CustomLayoutProps { + children: ReactNode; +} + +export function CustomLayout({ children }: CustomLayoutProps): ReactElement { + return
{children}
; +} + +export function toCustomLayoutHastNode({ children }: { children: ElementContent[] }): MdxJsxFlowElementHast { + return { + type: "mdxJsxFlowElement", + name: "CustomLayout", + attributes: [], + children, + }; +} diff --git a/packages/ui/app/src/layout/GuideLayout.tsx b/packages/ui/app/src/layout/GuideLayout.tsx new file mode 100644 index 0000000000..ec70254217 --- /dev/null +++ b/packages/ui/app/src/layout/GuideLayout.tsx @@ -0,0 +1,80 @@ +import { FernScrollArea } from "@fern-ui/components"; +import type { ElementContent } from "hast"; +import type { MdxJsxFlowElementHast } from "mdast-util-mdx-jsx"; +import { ReactNode, type ReactElement } from "react"; +import { BottomNavigationButtons } from "../components/BottomNavigationButtons"; +import { EditThisPageButton } from "../components/EditThisPage"; +import { PageHeader } from "../components/PageHeader"; +import { TableOfContents, TableOfContentsItem } from "../components/TableOfContents"; +import { Feedback } from "../custom-docs-page/Feedback"; +import { toAttribute } from "../mdx/plugins/utils"; +import { BuiltWithFern } from "../sidebar/BuiltWithFern"; + +interface GuideLayoutProps { + breadcrumbs: string[]; + title: string; + subtitle: ReactNode | undefined; + tableOfContents: TableOfContentsItem[] | undefined; + children: ReactNode; + editThisPageUrl: string | undefined; + hideFeedback: boolean | undefined; + hideNavLinks: boolean | undefined; +} + +interface GuideLayoutOpts { + breadcrumbs: string[]; + title: string; + subtitle: ElementContent | undefined; + tableOfContents: TableOfContentsItem[] | undefined; + children: ElementContent[]; + editThisPageUrl: string | undefined; + hideFeedback: boolean | undefined; + hideNavLinks: boolean | undefined; +} + +export function GuideLayout({ + breadcrumbs, + title, + tableOfContents, + subtitle, + children, + editThisPageUrl, + hideFeedback, + hideNavLinks, +}: GuideLayoutProps): ReactElement { + return ( +
+ {tableOfContents != null && ( + + )} +
+ +
{children}
+ {(!hideFeedback || !hideNavLinks || editThisPageUrl != null) && ( +
+
+
{!hideFeedback && }
+ +
+ + {!hideNavLinks && } + +
+ )} +
+
+ ); +} + +export function toGuideLayoutHastNode({ children, ...attributes }: GuideLayoutOpts): MdxJsxFlowElementHast { + return { + type: "mdxJsxFlowElement", + name: "GuideLayout", + attributes: Object.entries(attributes).map(([key, value]) => toAttribute(key, value)), + children, + }; +} diff --git a/packages/ui/app/src/layout/OverviewLayout.tsx b/packages/ui/app/src/layout/OverviewLayout.tsx new file mode 100644 index 0000000000..1369d75c65 --- /dev/null +++ b/packages/ui/app/src/layout/OverviewLayout.tsx @@ -0,0 +1,74 @@ +import { FernScrollArea } from "@fern-ui/components"; +import type { ElementContent } from "hast"; +import { MdxJsxFlowElementHast } from "mdast-util-mdx-jsx"; +import { ReactElement, ReactNode } from "react"; +import { EditThisPageButton } from "../components/EditThisPage"; +import { PageHeader } from "../components/PageHeader"; +import { TableOfContents, TableOfContentsItem } from "../components/TableOfContents"; +import { Feedback } from "../custom-docs-page/Feedback"; +import { toAttribute } from "../mdx/plugins/utils"; +import { BuiltWithFern } from "../sidebar/BuiltWithFern"; + +interface OverviewLayoutProps { + breadcrumbs: string[]; + title: string; + subtitle: ReactNode | undefined; + tableOfContents: TableOfContentsItem[] | undefined; + children: ReactNode; + editThisPageUrl: string | undefined; + hideFeedback: boolean | undefined; +} + +interface OverviewLayoutOpts { + breadcrumbs: string[]; + title: string; + subtitle: ElementContent | undefined; + tableOfContents: TableOfContentsItem[] | undefined; + children: ElementContent[]; + editThisPageUrl: string | undefined; + hideFeedback: boolean | undefined; +} + +export function OverviewLayout({ + breadcrumbs, + title, + tableOfContents, + subtitle, + children, + editThisPageUrl, + hideFeedback, +}: OverviewLayoutProps): ReactElement { + return ( +
+ {tableOfContents != null && ( + + )} +
+ +
{children}
+ {(!hideFeedback || editThisPageUrl != null) && ( +
+
+
{!hideFeedback && }
+ +
+ +
+ )} +
+
+ ); +} + +export function toOverviewLayoutHastNode({ children, ...attributes }: OverviewLayoutOpts): MdxJsxFlowElementHast { + return { + type: "mdxJsxFlowElement", + name: "OverviewLayout", + attributes: Object.entries(attributes).map(([key, value]) => toAttribute(key, value)), + children, + }; +} diff --git a/packages/ui/app/src/layout/PageLayout.tsx b/packages/ui/app/src/layout/PageLayout.tsx new file mode 100644 index 0000000000..68e0ac3f9f --- /dev/null +++ b/packages/ui/app/src/layout/PageLayout.tsx @@ -0,0 +1,65 @@ +import type { ElementContent } from "hast"; +import { MdxJsxFlowElementHast } from "mdast-util-mdx-jsx"; +import { ReactElement, ReactNode } from "react"; +import { EditThisPageButton } from "../components/EditThisPage"; +import { PageHeader } from "../components/PageHeader"; +import { TableOfContentsItem } from "../components/TableOfContents"; +import { Feedback } from "../custom-docs-page/Feedback"; +import { toAttribute } from "../mdx/plugins/utils"; +import { BuiltWithFern } from "../sidebar/BuiltWithFern"; + +interface PageLayoutProps { + breadcrumbs: string[]; + title: string; + subtitle: ReactNode | undefined; + tableOfContents: TableOfContentsItem[] | undefined; + children: ReactNode; + editThisPageUrl: string | undefined; + hideFeedback: boolean | undefined; + hideNavLinks: boolean | undefined; +} + +interface PageLayoutOpts { + breadcrumbs: string[]; + title: string; + subtitle: ElementContent | undefined; + tableOfContents: TableOfContentsItem[] | undefined; + children: ElementContent[]; + editThisPageUrl: string | undefined; + hideFeedback: boolean | undefined; + hideNavLinks: boolean | undefined; +} + +export function PageLayout({ + breadcrumbs, + title, + subtitle, + children, + editThisPageUrl, + hideFeedback, +}: PageLayoutProps): ReactElement { + return ( +
+ +
{children}
+ {(!hideFeedback || editThisPageUrl != null) && ( +
+
+
{!hideFeedback && }
+ +
+ +
+ )} +
+ ); +} + +export function toPageLayoutHastNode({ children, ...attributes }: PageLayoutOpts): MdxJsxFlowElementHast { + return { + type: "mdxJsxFlowElement", + name: "PageLayout", + attributes: Object.entries(attributes).map(([key, value]) => toAttribute(key, value)), + children, + }; +} diff --git a/packages/ui/app/src/layout/ReferenceLayout.tsx b/packages/ui/app/src/layout/ReferenceLayout.tsx new file mode 100644 index 0000000000..3528cc06c4 --- /dev/null +++ b/packages/ui/app/src/layout/ReferenceLayout.tsx @@ -0,0 +1,83 @@ +import clsx from "clsx"; +import type { ElementContent } from "hast"; +import { MdxJsxFlowElementHast } from "mdast-util-mdx-jsx"; +import { ReactElement, ReactNode, isValidElement } from "react"; +import { EditThisPageButton } from "../components/EditThisPage"; +import { PageHeader } from "../components/PageHeader"; +import { useApiPageContext } from "../contexts/useApiPageContext"; +import { Feedback } from "../custom-docs-page/Feedback"; +import { toAttribute } from "../mdx/plugins/utils"; +import { BuiltWithFern } from "../sidebar/BuiltWithFern"; + +interface ReferenceLayoutProps { + breadcrumbs: string[]; + title: string; + subtitle: ReactNode | undefined; + aside: ReactNode | undefined; + children: ReactNode; + editThisPageUrl: string | undefined; + hideFeedback: boolean | undefined; +} + +interface ReferenceLayoutOpts { + breadcrumbs: string[]; + title: string; + subtitle: ElementContent | undefined; + aside: ElementContent | undefined; + children: ElementContent[]; + editThisPageUrl: string | undefined; + hideFeedback: boolean | undefined; +} + +export function ReferenceLayout({ + breadcrumbs, + title, + subtitle, + aside, + children, + editThisPageUrl, + hideFeedback, +}: ReferenceLayoutProps): ReactElement { + const isApiPage = useApiPageContext(); + return ( +
+
+
+ +
+
{children}
+ {isValidElement(aside) && ( + + )} +
+ {(!hideFeedback || editThisPageUrl != null) && ( +
+
+
{!hideFeedback && }
+ +
+ {!isApiPage && } +
+ )} +
+
+
+ ); +} + +export function toReferenceLayoutHastNode({ children, ...attributes }: ReferenceLayoutOpts): MdxJsxFlowElementHast { + return { + type: "mdxJsxFlowElement", + name: "ReferenceLayout", + attributes: Object.entries(attributes).map(([key, value]) => toAttribute(key, value)), + children, + }; +} diff --git a/packages/ui/app/src/mdx/Markdown.tsx b/packages/ui/app/src/mdx/Markdown.tsx index ad7ee10220..ce47b0d503 100644 --- a/packages/ui/app/src/mdx/Markdown.tsx +++ b/packages/ui/app/src/mdx/Markdown.tsx @@ -1,7 +1,7 @@ import cn from "clsx"; import React from "react"; -import { SerializedMdxContent } from "./mdx"; import { MdxContent } from "./MdxContent"; +import { SerializedMdxContent } from "./mdx"; export declare namespace Markdown { export interface Props { @@ -17,12 +17,12 @@ export const Markdown = React.memo(function Markdown({ mdx, notP } return ( -
-
+ ); }); diff --git a/packages/ui/app/src/mdx/MdxContent.tsx b/packages/ui/app/src/mdx/MdxContent.tsx index 2c792fa5c1..3f26e05955 100644 --- a/packages/ui/app/src/mdx/MdxContent.tsx +++ b/packages/ui/app/src/mdx/MdxContent.tsx @@ -1,6 +1,7 @@ import { MDXRemote, type MDXRemoteSerializeResult } from "next-mdx-remote"; import React from "react"; import { FernErrorBoundary } from "../components/FernErrorBoundary"; +import { FrontmatterContextProvider } from "./frontmatter-context"; import type { SerializedMdxContent } from "./mdx"; import { HTML_COMPONENTS, JSX_COMPONENTS } from "./mdx-components"; @@ -25,12 +26,14 @@ export const MdxContent = React.memo(function MdxContent({ mdx return ( - + + + ); }); diff --git a/packages/ui/app/src/mdx/base-components.tsx b/packages/ui/app/src/mdx/base-components.tsx index 712c4e6fc6..822e7e5bb6 100644 --- a/packages/ui/app/src/mdx/base-components.tsx +++ b/packages/ui/app/src/mdx/base-components.tsx @@ -1,5 +1,6 @@ import { DocsV1Read } from "@fern-api/fdr-sdk"; import cn from "clsx"; +import { useAtomValue } from "jotai"; import { AnchorHTMLAttributes, Children, @@ -13,11 +14,12 @@ import { } from "react"; import Zoom from "react-medium-image-zoom"; import { useFeatureFlags } from "../atoms/flags"; +import { SLUG_ATOM } from "../atoms/location"; import { AbsolutelyPositionedAnchor } from "../commons/AbsolutelyPositionedAnchor"; import { FernImage } from "../components/FernImage"; import { FernLink } from "../components/FernLink"; import { useDocsContext } from "../contexts/docs-context/useDocsContext"; -import { useNavigationContext } from "../contexts/navigation-context"; +import { useFrontmatter } from "./frontmatter-context"; /** * By default, next will use /host/current/slug in SSG. @@ -25,8 +27,8 @@ import { useNavigationContext } from "../contexts/navigation-context"; * @returns /basepath/current/slug */ export function useCurrentPathname(): string { - const { resolvedPath } = useNavigationContext(); - return `/${resolvedPath.fullSlug}`; + const currentSlug = useAtomValue(SLUG_ATOM); + return `/${currentSlug}`; } export const HeadingRenderer = (level: number, props: ComponentProps<"h1">): ReactElement => { @@ -60,7 +62,7 @@ export const Ul: FC> = ({ className, ...rest }) => { }; export const Li: FC> = ({ className, ...rest }) => { - return
  • ; + return
  • ; }; export const A: FC> = ({ className, children, href, ...rest }) => { @@ -93,6 +95,7 @@ function isImgElement(element: ReactElement): element is ReactElement export const Image: FC = ({ className, src, width: w, height: h, noZoom, enableZoom, style, ...rest }) => { const { files } = useDocsContext(); + const { "no-image-zoom": noImageZoom } = useFrontmatter(); const fernImageSrc = useMemo((): DocsV1Read.File_ | undefined => { if (src == null) { @@ -127,7 +130,7 @@ export const Image: FC = ({ className, src, width: w, height: h, noZoo const { isImageZoomDisabled } = useFeatureFlags(); - if (isImageZoomDisabled ? !enableZoom : noZoom) { + if (isImageZoomDisabled || noImageZoom ? !enableZoom : noZoom) { return fernImage; } diff --git a/packages/ui/app/src/mdx/components/AccordionGroup.tsx b/packages/ui/app/src/mdx/components/AccordionGroup.tsx index b527593b41..9a177659b2 100644 --- a/packages/ui/app/src/mdx/components/AccordionGroup.tsx +++ b/packages/ui/app/src/mdx/components/AccordionGroup.tsx @@ -58,7 +58,7 @@ export const AccordionGroup: FC = ({ items = [], toc: paren diff --git a/packages/ui/app/src/mdx/components/CodeGroup.tsx b/packages/ui/app/src/mdx/components/CodeGroup.tsx index b7d58284a2..1f18a77730 100644 --- a/packages/ui/app/src/mdx/components/CodeGroup.tsx +++ b/packages/ui/app/src/mdx/components/CodeGroup.tsx @@ -21,7 +21,7 @@ export const CodeGroup: React.FC> = ({ const [selectedTabIndex, setSelectedTabIndex] = useState(0); const containerClass = clsx( - "after:ring-default bg-card relative mt-4 first:mt-0 mb-6 flex w-full min-w-0 max-w-full flex-col rounded-lg shadow-sm after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:ring-1 after:ring-inset after:content-['']", + "after:ring-card-border bg-card relative mt-4 first:mt-0 mb-6 flex w-full min-w-0 max-w-full flex-col rounded-lg shadow-sm after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:ring-1 after:ring-inset after:content-['']", { "dark bg-card-solid": isDarkCodeEnabled, }, diff --git a/packages/ui/app/src/mdx/components/HTMLTable/HTMLTable.tsx b/packages/ui/app/src/mdx/components/HTMLTable/HTMLTable.tsx index 2375e5462c..9da95f6d6b 100644 --- a/packages/ui/app/src/mdx/components/HTMLTable/HTMLTable.tsx +++ b/packages/ui/app/src/mdx/components/HTMLTable/HTMLTable.tsx @@ -8,6 +8,7 @@ import { ComponentProps, FC, useState } from "react"; const Table: FC> = ({ className, ...rest }) => { const [isFullScreen, setIsFullScreen] = useState(false); + return ( <> diff --git a/packages/ui/app/src/mdx/components/IFrame.tsx b/packages/ui/app/src/mdx/components/IFrame.tsx index 1737483119..7400a81b83 100644 --- a/packages/ui/app/src/mdx/components/IFrame.tsx +++ b/packages/ui/app/src/mdx/components/IFrame.tsx @@ -104,6 +104,7 @@ const ExperimentalIFrameWithFullscreen = ({ void iframe.requestFullscreen(); } }; + return ( diff --git a/packages/ui/app/src/mdx/components/RequestSnippet.tsx b/packages/ui/app/src/mdx/components/RequestSnippet.tsx index 55836c8f5a..ef2bf774ec 100644 --- a/packages/ui/app/src/mdx/components/RequestSnippet.tsx +++ b/packages/ui/app/src/mdx/components/RequestSnippet.tsx @@ -7,8 +7,8 @@ import { EndpointUrlWithOverflow } from "../../api-page/endpoints/EndpointUrlWit import { CodeSnippetExample } from "../../api-page/examples/CodeSnippetExample"; import { CodeExample, CodeExampleGroup, generateCodeExamples } from "../../api-page/examples/code-example"; import { FERN_LANGUAGE_ATOM } from "../../atoms/lang"; +import { useResolvedPath } from "../../atoms/navigation"; import { ApiReferenceButton } from "../../components/ApiReferenceButton"; -import { useNavigationContext } from "../../contexts/navigation-context"; import { ResolvedEndpointDefinition } from "../../resolver/types"; import { findEndpoint } from "../../util/processRequestSnippetComponents"; @@ -78,7 +78,7 @@ const EndpointRequestSnippetInternal: React.FC { - const { resolvedPath } = useNavigationContext(); + const resolvedPath = useResolvedPath(); const endpoint = useMemo(() => { if (resolvedPath.type !== "custom-markdown-page") { @@ -154,7 +154,7 @@ const EndpointResponseSnippetInternal: React.FC { - const { resolvedPath } = useNavigationContext(); + const resolvedPath = useResolvedPath(); const endpoint = useMemo(() => { if (resolvedPath.type !== "custom-markdown-page") { diff --git a/packages/ui/app/src/mdx/components/Steps.scss b/packages/ui/app/src/mdx/components/Steps.scss index 8419b73483..ef6884cf44 100644 --- a/packages/ui/app/src/mdx/components/Steps.scss +++ b/packages/ui/app/src/mdx/components/Steps.scss @@ -3,6 +3,10 @@ @apply ml-2 mb-12 mt-4 border-l border-default pl-7; counter-reset: step; + + .fern-steps > a { + @apply -ml-10; + } } .fern-steps h3 { @@ -10,7 +14,7 @@ &::before { @apply absolute size-[24px]; - @apply bg-card t-muted ring-1 ring-concealed backdrop-blur hover:ring-default; + @apply bg-card t-muted ring-1 ring-concealed backdrop-blur hover:ring-card-border; @apply rounded-md text-base font-normal text-center -indent-px; @apply ml-[-40px]; diff --git a/packages/ui/app/src/mdx/components/Tabs.tsx b/packages/ui/app/src/mdx/components/Tabs.tsx index c257d609e4..6e5e3cb492 100644 --- a/packages/ui/app/src/mdx/components/Tabs.tsx +++ b/packages/ui/app/src/mdx/components/Tabs.tsx @@ -35,7 +35,7 @@ export const TabGroup: FC = ({ tabs, toc: parentToc = true }) => return (
    diff --git a/packages/ui/app/src/mdx/frontmatter-context.tsx b/packages/ui/app/src/mdx/frontmatter-context.tsx new file mode 100644 index 0000000000..f974475d64 --- /dev/null +++ b/packages/ui/app/src/mdx/frontmatter-context.tsx @@ -0,0 +1,10 @@ +import { createContext, useContext } from "react"; +import { FernDocsFrontmatter } from "./frontmatter"; + +const FrontmatterContext = createContext({}); + +export const FrontmatterContextProvider = FrontmatterContext.Provider; + +export const useFrontmatter = (): FernDocsFrontmatter => { + return useContext(FrontmatterContext); +}; diff --git a/packages/ui/app/src/mdx/frontmatter.ts b/packages/ui/app/src/mdx/frontmatter.ts new file mode 100644 index 0000000000..9ff370add1 --- /dev/null +++ b/packages/ui/app/src/mdx/frontmatter.ts @@ -0,0 +1,134 @@ +import { DocsV1Read } from "@fern-api/fdr-sdk"; +import { JsonLd } from "@fern-ui/next-seo"; +import grayMatter from "gray-matter"; + +export declare namespace Layout { + /** + * The layout used for guides. This is the default layout. + * Guides are typically long-form content that is meant to be read from start to finish. + */ + type GuideLayout = "guide"; + + /** + * The layout used for overview pages. + * Overview pages are typically meant to be a landing page for a section of the documentation. + * These pages are 50% wider than guide pages, but the table of contents is still visible. + */ + type OverviewLayout = "overview"; + + /** + * Reference pages are best used for API docs or other material that is meant to have a right-hand column. + * Refrence pages are 2x the width of guide pages, and should be paired with