diff --git a/packages/commons/fdr-utils/src/traverser.ts b/packages/commons/fdr-utils/src/traverser.ts index 94c0355dad..beae4dae1e 100644 --- a/packages/commons/fdr-utils/src/traverser.ts +++ b/packages/commons/fdr-utils/src/traverser.ts @@ -97,7 +97,9 @@ function visitNode( } } - const apiSectionBreadcrumbs = [...sectionTitleBreadcrumbs, apiSection.title]; + const apiSectionBreadcrumbs = apiSection.isSidebarFlattened + ? sectionTitleBreadcrumbs + : [...sectionTitleBreadcrumbs, apiSection.title]; if (apiSection.changelog != null) { traverseState = visitPage(apiSection.changelog, currentNode, traverseState, apiSectionBreadcrumbs); diff --git a/packages/ui/app/src/api-page/ApiPage.tsx b/packages/ui/app/src/api-page/ApiPage.tsx index 03c4e9ebc3..1b1faca543 100644 --- a/packages/ui/app/src/api-page/ApiPage.tsx +++ b/packages/ui/app/src/api-page/ApiPage.tsx @@ -18,7 +18,6 @@ export const ApiPage: React.FC = ({ initialApi, showErrors }) => const hydrated = useIsReady(); const { isApiScrollingDisabled } = useFeatureFlags(); const setDefinitions = useSetAtom(APIS); - // const definition = apis[initialApi.api]; useEffect(() => { setDefinitions((prev) => ({ ...prev, [initialApi.api]: initialApi })); diff --git a/packages/ui/app/src/api-playground/PlaygroundContext.tsx b/packages/ui/app/src/api-playground/PlaygroundContext.tsx index 7a942cf5c8..a22534b9e6 100644 --- a/packages/ui/app/src/api-playground/PlaygroundContext.tsx +++ b/packages/ui/app/src/api-playground/PlaygroundContext.tsx @@ -2,12 +2,11 @@ import { useAtom } from "jotai"; import { atomWithStorage } from "jotai/utils"; import { mapValues, noop } from "lodash-es"; import dynamic from "next/dynamic"; -import { FC, PropsWithChildren, createContext, useCallback, useContext, useMemo, useState } from "react"; +import { FC, PropsWithChildren, createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; import urljoin from "url-join"; import { capturePosthogEvent } from "../analytics/posthog"; import { useFeatureFlags } from "../contexts/FeatureFlagContext"; import { useDocsContext } from "../contexts/docs-context/useDocsContext"; -import { useNavigationContext } from "../contexts/navigation-context"; import { ResolvedApiDefinition, ResolvedRootPackage, @@ -53,10 +52,29 @@ export const PLAYGROUND_FORM_STATE_ATOM = atomWithStorage = ({ children }) => { const { isApiPlaygroundEnabled } = useFeatureFlags(); const [apis, setApis] = useAtom(APIS); - const { basePath } = useDocsContext(); - const { selectedSlug } = useNavigationContext(); + const { basePath, apis: apiIds } = useDocsContext(); const [selectionState, setSelectionState] = useState(); + useEffect(() => { + const unfetchedApis = apiIds.filter((apiId) => apis[apiId] == null); + if (unfetchedApis.length === 0) { + return; + } + + void Promise.all( + unfetchedApis.map(async (apiId) => { + const r = await fetch( + urljoin(basePath ?? "", "/api/fern-docs/resolve-api?path=" + (basePath ?? "/") + "&api=" + apiId), + ); + const data: Record | null = await r.json(); + if (data == null) { + return; + } + setApis((currentApis) => ({ ...currentApis, ...data })); + }), + ); + }, [apiIds, apis, basePath, setApis]); + const flattenedApis = useMemo(() => mapValues(apis, flattenRootPackage), [apis]); const [isPlaygroundOpen, setPlaygroundOpen] = useAtom(PLAYGROUND_OPEN_ATOM); @@ -75,23 +93,9 @@ export const PlaygroundContextProvider: FC = ({ children }) = const setSelectionStateAndOpen = useCallback( async (newSelectionState: PlaygroundSelectionState) => { - let matchedPackage = flattenedApis[newSelectionState.api]; + const matchedPackage = flattenedApis[newSelectionState.api]; if (matchedPackage == null) { - const r = await fetch( - urljoin( - basePath ?? "", - "/api/fern-docs/resolve-api?path=/" + selectedSlug + "&api=" + newSelectionState.api, - ), - ); - - const data: ResolvedRootPackage | null = await r.json(); - - if (data == null) { - return; - } - - setApis((currentApis) => ({ ...currentApis, [newSelectionState.api]: data })); - matchedPackage = flattenRootPackage(data); + return; } if (newSelectionState.type === "endpoint") { @@ -131,7 +135,7 @@ export const PlaygroundContextProvider: FC = ({ children }) = }); } }, - [basePath, expandPlayground, flattenedApis, globalFormState, selectedSlug, setApis, setGlobalFormState], + [expandPlayground, flattenedApis, globalFormState, setGlobalFormState], ); if (!isApiPlaygroundEnabled) { diff --git a/packages/ui/app/src/api-playground/PlaygroundDrawer.tsx b/packages/ui/app/src/api-playground/PlaygroundDrawer.tsx index 9fa1846ec0..dc8bce2fdd 100644 --- a/packages/ui/app/src/api-playground/PlaygroundDrawer.tsx +++ b/packages/ui/app/src/api-playground/PlaygroundDrawer.tsx @@ -1,16 +1,14 @@ import { APIV1Read, FdrAPI } from "@fern-api/fdr-sdk"; import { EMPTY_OBJECT, visitDiscriminatedUnion } from "@fern-ui/core-utils"; -import { Portal, Transition } from "@headlessui/react"; -import { Cross1Icon } from "@radix-ui/react-icons"; -import { TooltipProvider } from "@radix-ui/react-tooltip"; -import clsx from "clsx"; +// import { Portal, Transition } from "@headlessui/react"; +import * as Dialog from "@radix-ui/react-dialog"; +import { ArrowLeftIcon, Cross1Icon } from "@radix-ui/react-icons"; import { atom, useAtom } from "jotai"; import { mapValues } from "lodash-es"; import { Dispatch, FC, SetStateAction, useCallback, useEffect, useMemo } from "react"; import { capturePosthogEvent } from "../analytics/posthog"; -import { FernButton, FernButtonGroup } from "../components/FernButton"; +import { FernButton } from "../components/FernButton"; import { FernErrorBoundary } from "../components/FernErrorBoundary"; -import { FernTooltip, FernTooltipProvider } from "../components/FernTooltip"; import { useDocsContext } from "../contexts/docs-context/useDocsContext"; import { useLayoutBreakpoint } from "../contexts/layout-breakpoint/useLayoutBreakpoint"; import { @@ -25,9 +23,9 @@ import { } from "../resolver/types"; import { PLAYGROUND_FORM_STATE_ATOM, PLAYGROUND_OPEN_ATOM, usePlaygroundContext } from "./PlaygroundContext"; import { PlaygroundEndpoint } from "./PlaygroundEndpoint"; -import { PlaygroundEndpointSelector } from "./PlaygroundEndpointSelector"; import { PlaygroundEndpointSelectorContent, flattenApiSection } from "./PlaygroundEndpointSelectorContent"; import { PlaygroundWebSocket } from "./PlaygroundWebSocket"; +import { HorizontalSplitPane } from "./VerticalSplitPane"; import { PlaygroundEndpointRequestFormState, PlaygroundFormDataEntryValue, @@ -85,7 +83,9 @@ export function usePlaygroundHeight(): [number, Dispatch> const [playgroundHeight, setHeight] = useAtom(PLAYGROUND_HEIGHT_ATOM); const windowHeight = useWindowHeight(); const height = - windowHeight != null ? Math.max(Math.min(windowHeight - headerHeight, playgroundHeight), 40) : playgroundHeight; + windowHeight != null + ? Math.max(Math.min(windowHeight - headerHeight, playgroundHeight), windowHeight / 3) + : playgroundHeight; return [height, setHeight]; } @@ -95,7 +95,7 @@ interface PlaygroundDrawerProps { } export const PlaygroundDrawer: FC = ({ apis }) => { - const { selectionState, hasPlayground, collapsePlayground } = usePlaygroundContext(); + const { selectionState, hasPlayground } = usePlaygroundContext(); const windowHeight = useWindowHeight(); const { sidebarNodes } = useDocsContext(); @@ -249,164 +249,101 @@ export const PlaygroundDrawer: FC = ({ apis }) => { }; }, [togglePlayground]); + const { endpoint: selectedEndpoint } = apiGroups + .flatMap((group) => [ + ...group.items + .filter((item) => item.apiType === "endpoint" || item.apiType === "websocket") + .map((endpoint) => ({ group, endpoint })), + ]) + .find(({ endpoint }) => + selectionState?.type === "endpoint" + ? endpoint.slug.join("/") === selectionState?.endpointId + : selectionState?.type === "websocket" + ? endpoint.slug.join("/") === selectionState?.webSocketId + : false, + ) ?? { + endpoint: undefined, + group: undefined, + }; + if (!hasPlayground) { return null; } - const mobileHeader = ( -
-
-
- {selectionState != null ? ( - - ) : ( -
Select an endpoint to get started
- )} -
-
- -
- - - - Close API Playground - CTRL + ` - - } - > - } - onClick={collapsePlayground} - rounded - /> - - - -
-
- ); - - const desktopHeader = ( -
-
- - API Playground - -
- -
- {selectionState != null ? ( - - ) : ( -
Select an endpoint to get started
- )} -
- -
- - - - Close API Playground - CTRL + ` - - } - > - } - onClick={collapsePlayground} - rounded - /> - - - -
-
- ); - return ( - - - {layoutBreakpoint !== "mobile" && ( -
-
-
-
-
-
- )} -
-
{layoutBreakpoint === "mobile" ? mobileHeader : desktopHeader}
- + + + { + e.preventDefault(); + }} > - {selectionState?.type === "endpoint" && matchedEndpoint != null ? ( - - ) : selectionState?.type === "websocket" && matchedWebSocket != null ? ( - - ) : ( - -
- + {layoutBreakpoint !== "mobile" && ( + <> +
+
+
+
+
- + + } size="large" rounded variant="minimal" /> + + )} - -
- - + + + + {selectionState?.type === "endpoint" && matchedEndpoint != null ? ( + + ) : selectionState?.type === "websocket" && matchedWebSocket != null ? ( + + ) : ( +
+ +
Select an endpoint to get started
+
+ )} +
+ + + + ); }; diff --git a/packages/ui/app/src/api-playground/PlaygroundEndpoint.css b/packages/ui/app/src/api-playground/PlaygroundEndpoint.css index 68fad8756d..b62225788c 100644 --- a/packages/ui/app/src/api-playground/PlaygroundEndpoint.css +++ b/packages/ui/app/src/api-playground/PlaygroundEndpoint.css @@ -1,6 +1,6 @@ .playground-endpoint { @apply flex min-w-0 flex-1 shrink items-start gap-2; - @apply px-2 w-full; + @apply p-3 pb-0 w-full; } .playground-endpoint-url { diff --git a/packages/ui/app/src/api-playground/PlaygroundEndpoint.tsx b/packages/ui/app/src/api-playground/PlaygroundEndpoint.tsx index 8485a2d222..6cddd2722a 100644 --- a/packages/ui/app/src/api-playground/PlaygroundEndpoint.tsx +++ b/packages/ui/app/src/api-playground/PlaygroundEndpoint.tsx @@ -162,7 +162,7 @@ export const PlaygroundEndpoint: FC = ({ return ( -
+
= ({ endpoint, formState, setFormState, - resetWithExample, - resetWithoutExample, + // resetWithExample, + // resetWithoutExample, response, sendRequest, types, @@ -90,23 +89,12 @@ export const PlaygroundEndpointContent: FC = ({ /> )} -
- - -
+
); diff --git a/packages/ui/app/src/api-playground/PlaygroundEndpointSelectorContent.tsx b/packages/ui/app/src/api-playground/PlaygroundEndpointSelectorContent.tsx index 17b6bcc63a..1ee9b263eb 100644 --- a/packages/ui/app/src/api-playground/PlaygroundEndpointSelectorContent.tsx +++ b/packages/ui/app/src/api-playground/PlaygroundEndpointSelectorContent.tsx @@ -2,15 +2,16 @@ import { FdrAPI } from "@fern-api/fdr-sdk"; import { isNonNullish, visitDiscriminatedUnion } from "@fern-ui/core-utils"; import { SidebarNode } from "@fern-ui/fdr-utils"; import { Cross1Icon, MagnifyingGlassIcon, SlashIcon } from "@radix-ui/react-icons"; -import cn from "clsx"; +import cn, { clsx } from "clsx"; import { compact, noop } from "lodash-es"; import dynamic from "next/dynamic"; -import { Fragment, ReactElement, forwardRef, useImperativeHandle, useRef, useState } from "react"; +import { Fragment, ReactElement, forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react"; import { HttpMethodTag } from "../commons/HttpMethodTag"; import { FernButton } from "../components/FernButton"; import { FernInput } from "../components/FernInput"; import { FernScrollArea } from "../components/FernScrollArea"; -import { FernTooltip } from "../components/FernTooltip"; +import { FernTooltip, FernTooltipProvider } from "../components/FernTooltip"; +import { BuiltWithFern } from "../sidebar/BuiltWithFern"; import { usePlaygroundContext } from "./PlaygroundContext"; const Markdown = dynamic(() => import("../mdx/Markdown").then(({ Markdown }) => Markdown), { ssr: true }); @@ -49,7 +50,7 @@ export function flattenApiSection(navigation: SidebarNode[]): ApiGroup[] { result.push({ api: apiSection.api, id: apiSection.id, - breadcrumbs: [apiSection.title], + breadcrumbs: apiSection.isSidebarFlattened ? [] : [apiSection.title], items: apiSection.items .filter((item): item is SidebarNode.ApiPage => item.type === "page") .flatMap((item): SidebarNode.ApiPage[] => @@ -64,7 +65,9 @@ export function flattenApiSection(navigation: SidebarNode[]): ApiGroup[] { ), ).map((group) => ({ ...group, - breadcrumbs: [apiSection.title, ...group.breadcrumbs], + breadcrumbs: apiSection.isSidebarFlattened + ? group.breadcrumbs + : [apiSection.title, ...group.breadcrumbs], })), ); }, @@ -84,13 +87,21 @@ function matchesEndpoint(query: string, group: ApiGroup, endpoint: SidebarNode.A } export const PlaygroundEndpointSelectorContent = forwardRef( - function PlaygroundEndpointSelectorContent({ apiGroups, closeDropdown, selectedEndpoint, className }, ref) { + ({ apiGroups, closeDropdown, selectedEndpoint, className }, ref) => { + const scrollRef = useRef(null); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + useImperativeHandle(ref, () => scrollRef.current!); + const { setSelectionStateAndOpen } = usePlaygroundContext(); const [filterValue, setFilterValue] = useState(""); const selectedItemRef = useRef(null); + useEffect(() => { + selectedItemRef.current?.scrollIntoView({ block: "center" }); + }, []); + const createSelectEndpoint = (group: ApiGroup, endpoint: SidebarNode.EndpointPage) => () => { setSelectionStateAndOpen({ type: "endpoint", @@ -117,9 +128,9 @@ export const PlaygroundEndpointSelectorContent = forwardRef +
  • {apiGroup.breadcrumbs.length > 0 && ( -
    +
    {apiGroup.breadcrumbs.map((breadcrumb, idx) => ( {idx > 0 && } @@ -147,12 +158,18 @@ export const PlaygroundEndpointSelectorContent = forwardRef } + rightIcon={ + + } />
  • @@ -190,45 +207,52 @@ export const PlaygroundEndpointSelectorContent = forwardRef renderApiDefinitionPackage(group)).filter(isNonNullish); - const menuRef = useRef(null); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - useImperativeHandle(ref, () => menuRef.current!); - return ( -
    -
    0, - })} - > - } - data-1p-ignore="true" - autoFocus={true} - value={filterValue} - onValueChange={setFilterValue} - rightElement={ - filterValue.length > 0 && ( - } - variant="minimal" - onClick={() => setFilterValue("")} - /> - ) - } - /> + +
    +
    + } + data-1p-ignore="true" + autoFocus={true} + value={filterValue} + onValueChange={setFilterValue} + rightElement={ + filterValue.length > 0 && ( + } + variant="minimal" + onClick={() => setFilterValue("")} + /> + ) + } + placeholder="Search for endpoints..." + /> +
    + +
      {renderedListItems}
    +
    +
    +
    - -
      {renderedListItems}
    -
    -
    + ); }, ); +PlaygroundEndpointSelectorContent.displayName = "PlaygroundEndpointSelectorContent"; + function renderTextWithHighlight(text: string, highlight: string): ReactElement[] { highlight = highlight.trim(); + if (highlight === "") { + return [{text}]; + } // Split text on higlight term, include term itself into parts, ignore case const parts = text.split(new RegExp(`(${highlight})`, "gi")); return parts.map((part, idx) => diff --git a/packages/ui/app/src/api-playground/VerticalSplitPane.tsx b/packages/ui/app/src/api-playground/VerticalSplitPane.tsx index 04cd43143e..26e669fcf4 100644 --- a/packages/ui/app/src/api-playground/VerticalSplitPane.tsx +++ b/packages/ui/app/src/api-playground/VerticalSplitPane.tsx @@ -1,4 +1,4 @@ -import cn from "clsx"; +import cn, { clsx } from "clsx"; import { Children, ComponentProps, PropsWithChildren, ReactElement, useCallback, useRef, useState } from "react"; import { useHorizontalSplitPane, useVerticalSplitPane } from "./useSplitPlane"; @@ -63,6 +63,8 @@ interface HorizontalSplitPaneProps extends ComponentProps<"div"> { leftClassName?: string; rightClassName?: string; rizeBarHeight?: number; + mode?: "pixel" | "percent"; + initialLeftWidth?: number; } export function HorizontalSplitPane({ @@ -71,18 +73,23 @@ export function HorizontalSplitPane({ rightClassName, children, rizeBarHeight, + mode = "percent", + initialLeftWidth = mode === "percent" ? 0.6 : 300, ...props }: PropsWithChildren): ReactElement | null { - const [leftHeightPercent, setLeftHeightPercent] = useState(0.6); + const [leftWidth, setLeftWidth] = useState(initialLeftWidth); const ref = useRef(null); - const setWidth = useCallback((clientX: number) => { - if (ref.current != null) { - const { left, width } = ref.current.getBoundingClientRect(); - setLeftHeightPercent((clientX - left - 6) / width); - } - }, []); + const setWidth = useCallback( + (clientX: number) => { + if (ref.current != null) { + const { left, width } = ref.current.getBoundingClientRect(); + setLeftWidth(mode === "percent" ? (clientX - left - 6) / width : clientX - left - 6); + } + }, + [mode], + ); const handleVerticalResize = useHorizontalSplitPane(setWidth); @@ -104,13 +111,22 @@ export function HorizontalSplitPane({ return (
    -
    +
    {left}
    diff --git a/packages/ui/app/src/api-playground/form/PlaygroundTypeReferenceForm.tsx b/packages/ui/app/src/api-playground/form/PlaygroundTypeReferenceForm.tsx index ff7cc8ba88..78fb8c3a46 100644 --- a/packages/ui/app/src/api-playground/form/PlaygroundTypeReferenceForm.tsx +++ b/packages/ui/app/src/api-playground/form/PlaygroundTypeReferenceForm.tsx @@ -349,12 +349,12 @@ export const PlaygroundTypeReferenceForm = memo ( - {stringLiteral.value} + {stringLiteral.value} ), booleanLiteral: (stringLiteral) => ( - {stringLiteral.value ? "TRUE" : "FALSE"} + {stringLiteral.value ? "true" : "false"} ), _other: () => null, diff --git a/packages/ui/app/src/api-playground/useSplitPlane.ts b/packages/ui/app/src/api-playground/useSplitPlane.ts index 5ff6cab589..c20a685ce7 100644 --- a/packages/ui/app/src/api-playground/useSplitPlane.ts +++ b/packages/ui/app/src/api-playground/useSplitPlane.ts @@ -68,6 +68,11 @@ export function useHorizontalSplitPane( ): (e: React.MouseEvent) => void { return useCallback( (e: React.MouseEvent) => { + // disable if the event is not a left click + if (e.button !== 0) { + return; + } + e.preventDefault(); e.stopPropagation(); const handleMouseMove = (e: MouseEvent | TouchEvent) => { diff --git a/packages/ui/app/src/commons/HttpMethodTag.tsx b/packages/ui/app/src/commons/HttpMethodTag.tsx index 89504ecb9f..3bfb9bc596 100644 --- a/packages/ui/app/src/commons/HttpMethodTag.tsx +++ b/packages/ui/app/src/commons/HttpMethodTag.tsx @@ -32,7 +32,7 @@ const UnmemoizedHttpMethodTag: React.FC = ({ diff --git a/packages/ui/app/src/components/FernTag.tsx b/packages/ui/app/src/components/FernTag.tsx index 0ac82e6909..f2b8b572cc 100644 --- a/packages/ui/app/src/components/FernTag.tsx +++ b/packages/ui/app/src/components/FernTag.tsx @@ -55,11 +55,11 @@ export const FernTag: FC = ({ // Green "bg-green-a3 text-green-a11": colorScheme === "green" && variant === "subtle", - "bg-green-a10 text-green-1": colorScheme === "green" && variant === "solid", + "bg-green-a10 text-green-1 dark:text-green-12": colorScheme === "green" && variant === "solid", // Blue "bg-blue-a3 text-blue-a11": colorScheme === "blue" && variant === "subtle", - "bg-blue-a10 text-blue-1": colorScheme === "blue" && variant === "solid", + "bg-blue-a10 text-blue-1 dark:text-blue-12": colorScheme === "blue" && variant === "solid", // Amber "bg-amber-a3 text-amber-a11": colorScheme === "amber" && variant === "subtle", @@ -67,7 +67,7 @@ export const FernTag: FC = ({ // Red "bg-red-a3 text-red-a11": colorScheme === "red" && variant === "subtle", - "bg-red-a10 text-red-1": colorScheme === "red" && variant === "solid", + "bg-red-a10 text-red-1 dark:text-red-12": colorScheme === "red" && variant === "solid", // Accent "bg-accent/20 text-accent-aaa": colorScheme === "accent" && variant === "subtle", diff --git a/packages/ui/app/src/contexts/docs-context/DocsContext.ts b/packages/ui/app/src/contexts/docs-context/DocsContext.ts index 80e10d92b6..de0e9ddb95 100644 --- a/packages/ui/app/src/contexts/docs-context/DocsContext.ts +++ b/packages/ui/app/src/contexts/docs-context/DocsContext.ts @@ -1,4 +1,4 @@ -import { DocsV1Read } from "@fern-api/fdr-sdk"; +import { DocsV1Read, FdrAPI } from "@fern-api/fdr-sdk"; import { ColorsConfig, SidebarNavigation } from "@fern-ui/fdr-utils"; import React from "react"; @@ -21,6 +21,7 @@ export const DocsContext = React.createContext({ sidebarNodes: [], searchInfo: undefined, navbarLinks: [], + apis: [], }); export interface DocsContextValue extends SidebarNavigation { @@ -33,6 +34,7 @@ export interface DocsContextValue extends SidebarNavigation { files: Record; searchInfo: DocsV1Read.SearchInfo | undefined; navbarLinks: DocsV1Read.NavbarLink[]; + apis: FdrAPI.ApiDefinitionId[]; resolveFile: (fileId: DocsV1Read.FileId) => DocsV1Read.File_ | undefined; } diff --git a/packages/ui/app/src/contexts/docs-context/DocsContextProvider.tsx b/packages/ui/app/src/contexts/docs-context/DocsContextProvider.tsx index cf1486988c..28ce77ec99 100644 --- a/packages/ui/app/src/contexts/docs-context/DocsContextProvider.tsx +++ b/packages/ui/app/src/contexts/docs-context/DocsContextProvider.tsx @@ -28,6 +28,7 @@ export const DocsContextProvider: React.FC = ({ child const versions = useDeepCompareMemoize(pageProps.navigation.versions); const searchInfo = useDeepCompareMemoize(pageProps.search); const navbarLinks = useDeepCompareMemoize(pageProps.navbarLinks); + const apis = useDeepCompareMemoize(pageProps.apis); const { resolvedTheme: theme } = useTheme(); const { baseUrl, title, favicon } = pageProps; @@ -67,6 +68,7 @@ export const DocsContextProvider: React.FC = ({ child sidebarNodes, searchInfo, navbarLinks, + apis, }), [ baseUrl.basePath, @@ -84,6 +86,7 @@ export const DocsContextProvider: React.FC = ({ child versions, searchInfo, navbarLinks, + apis, ], ); diff --git a/packages/ui/app/src/docs/Docs.tsx b/packages/ui/app/src/docs/Docs.tsx index b94a307cc7..43e2dac86f 100644 --- a/packages/ui/app/src/docs/Docs.tsx +++ b/packages/ui/app/src/docs/Docs.tsx @@ -102,7 +102,7 @@ export const Docs: React.FC = memo(function UnmemoizedDocs docsMainContent )} - +
    {/* Enables footer DOM injection */} diff --git a/packages/ui/app/src/mdx/MdxContent.tsx b/packages/ui/app/src/mdx/MdxContent.tsx index 350e892954..2c792fa5c1 100644 --- a/packages/ui/app/src/mdx/MdxContent.tsx +++ b/packages/ui/app/src/mdx/MdxContent.tsx @@ -10,9 +10,9 @@ export declare namespace MdxContent { } } -const COMPONENTS = { ...HTML_COMPONENTS, ...JSX_COMPONENTS }; - export const MdxContent = React.memo(function MdxContent({ mdx }) { + const COMPONENTS = { ...HTML_COMPONENTS, ...JSX_COMPONENTS }; + if (typeof mdx === "string") { return {mdx}; } diff --git a/packages/ui/app/src/next-app/DocsPage.tsx b/packages/ui/app/src/next-app/DocsPage.tsx index 6fa1f14815..17fb3c498e 100644 --- a/packages/ui/app/src/next-app/DocsPage.tsx +++ b/packages/ui/app/src/next-app/DocsPage.tsx @@ -1,4 +1,4 @@ -import { DocsV1Read, DocsV2Read } from "@fern-api/fdr-sdk"; +import { DocsV1Read, DocsV2Read, FdrAPI } from "@fern-api/fdr-sdk"; import type { ColorsConfig, SidebarNavigation } from "@fern-ui/fdr-utils"; import { useDeepCompareMemoize } from "@fern-ui/react-commons"; import { Redirect } from "next"; @@ -33,6 +33,7 @@ export declare namespace DocsPage { resolvedPath: ResolvedPath; featureFlags: FeatureFlags; + apis: FdrAPI.ApiDefinitionId[]; } } diff --git a/packages/ui/app/src/next-app/globals.scss b/packages/ui/app/src/next-app/globals.scss index b0dd03a29e..85a86eb975 100644 --- a/packages/ui/app/src/next-app/globals.scss +++ b/packages/ui/app/src/next-app/globals.scss @@ -490,9 +490,17 @@ &:not(.disabled) { &.minimal { @apply bg-transparent; - @apply t-muted hover:t-default; + // @apply t-muted hover:t-default; @apply hover:bg-tag-default data-[state=on]:bg-tag-default data-[state=checked]:bg-tag-default data-[state=open]:bg-tag-default data-[state=opening]:bg-tag-default data-[selected=true]:bg-tag-default; + .fern-button-text { + @apply t-muted; + + &:hover { + @apply t-default; + } + } + .fa-icon { @apply bg-text-default/60; } diff --git a/packages/ui/app/src/sidebar/BuiltWithFern.tsx b/packages/ui/app/src/sidebar/BuiltWithFern.tsx index c1e7f2773a..e22471af69 100644 --- a/packages/ui/app/src/sidebar/BuiltWithFern.tsx +++ b/packages/ui/app/src/sidebar/BuiltWithFern.tsx @@ -25,13 +25,13 @@ export const BuiltWithFern: React.FC = ({ className }) => { } return ( -
    +
    Built with diff --git a/packages/ui/docs-bundle/src/pages/_error.tsx b/packages/ui/docs-bundle/src/pages/_error.tsx index 2689ceb0c4..3addf95fd6 100644 --- a/packages/ui/docs-bundle/src/pages/_error.tsx +++ b/packages/ui/docs-bundle/src/pages/_error.tsx @@ -8,6 +8,8 @@ export function parseResolvedUrl(resolvedUrl: string): string { return match?.[2] ?? resolvedUrl; } +export const dynamic = "force-dynamic"; + export const getServerSideProps: GetServerSideProps = async ({ req, res, resolvedUrl, query }) => { if ( res.statusCode >= 500 && @@ -27,7 +29,7 @@ export const getServerSideProps: GetServerSideProps = async ({ req, return { props: { statusCode: res.statusCode, - title: res.statusMessage, + title: res.statusMessage ?? "", }, }; }; diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/resolve-api.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/resolve-api.ts index 165feef8cd..bf65cde5b4 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/resolve-api.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/resolve-api.ts @@ -1,14 +1,14 @@ -import { flattenApiDefinition, getNavigationRoot, type SidebarNode } from "@fern-ui/fdr-utils"; -import { - ApiDefinitionResolver, - REGISTRY_SERVICE, - serializeSidebarNodeDescriptionMdx, - type ResolvedRootPackage, -} from "@fern-ui/ui"; +import { resolveSidebarNodesRoot, visitSidebarNodeRaw } from "@fern-ui/fdr-utils"; +import { ApiDefinitionResolver, REGISTRY_SERVICE, type ResolvedRootPackage } from "@fern-ui/ui"; import { NextApiHandler, NextApiResponse } from "next"; import { getFeatureFlags } from "./feature-flags"; -const resolveApiHandler: NextApiHandler = async (req, res: NextApiResponse) => { +export const dynamic = "force-dynamic"; + +const resolveApiHandler: NextApiHandler = async ( + req, + res: NextApiResponse | null>, +) => { try { if (req.method !== "GET") { res.status(400).json(null); @@ -35,58 +35,57 @@ const resolveApiHandler: NextApiHandler = async (req, res: NextApiResponse t.length > 0) : []; - const sidebarNodes = await Promise.all( - navigation.found.sidebarNodes.map((node) => serializeSidebarNodeDescriptionMdx(node)), + const root = resolveSidebarNodesRoot( + docsConfig.navigation, + docs.definition.apis, + docs.definition.pages, + basePathSlug, + docs.baseUrl.domain, ); - const apiSection = findApiSection(api, sidebarNodes); - - const featureFlags = await getFeatureFlags(docs.body.baseUrl.domain); + const entryPromises: Promise<[string, ResolvedRootPackage]>[] = []; + + const featureFlags = await getFeatureFlags(docs.baseUrl.domain); + + visitSidebarNodeRaw(root, (node) => { + if (node.type === "apiSection" && node.flattenedApiDefinition != null) { + const entry = ApiDefinitionResolver.resolve( + node.title, + node.flattenedApiDefinition, + docsDefinition.pages, + undefined, + featureFlags, + docs.baseUrl.domain, + ).then((resolved) => [node.api, resolved] as [string, ResolvedRootPackage]); + entryPromises.push(entry); + return "skip"; + } + return undefined; + }); - res.status(200).json( - await ApiDefinitionResolver.resolve( - apiSection?.title ?? "", - flattenApiDefinition(apiDefinition, apiSection?.slug ?? [], undefined, docs.body.baseUrl.domain), - pages, - undefined, - featureFlags, - docs.body.baseUrl.domain, - ), - ); + res.status(200).json(Object.fromEntries(await Promise.all(entryPromises))); } catch (err) { // eslint-disable-next-line no-console console.error(err); @@ -96,16 +95,16 @@ const resolveApiHandler: NextApiHandler = async (req, res: NextApiResponse