diff --git a/packages/ui/app/src/atoms/playground.ts b/packages/ui/app/src/atoms/playground.ts index 669be26f2d..57e5cd5429 100644 --- a/packages/ui/app/src/atoms/playground.ts +++ b/packages/ui/app/src/atoms/playground.ts @@ -37,7 +37,7 @@ import { FEATURE_FLAGS_ATOM } from "./flags"; import { useAtomEffect } from "./hooks"; import { HEADER_HEIGHT_ATOM } from "./layout"; import { LOCATION_ATOM } from "./location"; -import { NAVIGATION_NODES_ATOM } from "./navigation"; +import { CURRENT_NODE_ATOM, NAVIGATION_NODES_ATOM } from "./navigation"; import { atomWithStorageValidation } from "./utils/atomWithStorageValidation"; import { IS_MOBILE_SCREEN_ATOM } from "./viewport"; @@ -136,14 +136,14 @@ export function useClosePlayground(): () => void { }); } -export function useOpenPlayground(): (nodeId?: FernNavigation.NodeId) => void { - const setNodeId = useSetAtom(PLAYGROUND_NODE_ID); - const prevNodeId = useAtomValue(PREV_PLAYGROUND_NODE_ID); - return useEventCallback((nodeId?: FernNavigation.NodeId) => { - // TODO: "" implicitly means open + empty state. This is a hack and we should rethink the UX. - setNodeId(nodeId ?? prevNodeId ?? FernNavigation.NodeId("")); - }); -} +// export function useOpenPlayground(): (nodeId?: FernNavigation.NodeId) => void { +// const setNodeId = useSetAtom(PLAYGROUND_NODE_ID); +// const prevNodeId = useAtomValue(PREV_PLAYGROUND_NODE_ID); +// return useEventCallback((nodeId?: FernNavigation.NodeId) => { +// // TODO: "" implicitly means open + empty state. This is a hack and we should rethink the UX. +// setNodeId(nodeId ?? prevNodeId ?? FernNavigation.NodeId("")); +// }); +// } export function useTogglePlayground(): () => void { const isPlaygroundOpen = useIsPlaygroundOpen(); @@ -153,7 +153,7 @@ export function useTogglePlayground(): () => void { if (isPlaygroundOpen) { closePlayground(); } else { - openPlayground(); + void openPlayground(); } }); } @@ -345,12 +345,42 @@ export const usePlaygroundFormStateAtom = ( return formStateAtom; }; -export function useSetAndOpenPlayground(): (node: FernNavigation.NavigationNodeApiLeaf) => Promise { +const API_LEAF_NODES = atom((get) => get(NAVIGATION_NODES_ATOM).getNodesInOrder().filter(FernNavigation.isApiLeaf)); +export const HAS_API_PLAYGROUND = atom((get) => get(IS_PLAYGROUND_ENABLED_ATOM) && get(API_LEAF_NODES).length > 0); + +export function useOpenPlayground(): (node?: FernNavigation.NavigationNodeApiLeaf) => Promise { const preload = usePreloadApiLeaf(); return useAtomCallback( useCallbackOne( - async (get, set, node: FernNavigation.NavigationNodeApiLeaf) => { + async (get, set, node?: FernNavigation.NavigationNodeApiLeaf) => { + if (!get(HAS_API_PLAYGROUND)) { + set(PLAYGROUND_NODE_ID, undefined); + return; + } + + if (node == null) { + const prevNodeId = get(PREV_PLAYGROUND_NODE_ID); + if (prevNodeId != null && get(API_LEAF_NODES).some((n) => n.id === prevNodeId)) { + set(PLAYGROUND_NODE_ID, prevNodeId); + return; + } + + // if no previous node, use the current node (if it's an API leaf) + const currentNode = get(CURRENT_NODE_ATOM); + if (currentNode != null && FernNavigation.isApiLeaf(currentNode)) { + set(PLAYGROUND_NODE_ID, currentNode.id); + return; + } + + // get the first API leaf node + const firstApiLeafNode = get(API_LEAF_NODES)[0]; + if (firstApiLeafNode != null) { + set(PLAYGROUND_NODE_ID, firstApiLeafNode.id); + } + return; + } + const formStateAtom = playgroundFormStateFamily(node.id); set(PLAYGROUND_NODE_ID, node.id); diff --git a/packages/ui/app/src/playground/PlaygroundButton.tsx b/packages/ui/app/src/playground/PlaygroundButton.tsx index dedd1b21dd..0234f8700d 100644 --- a/packages/ui/app/src/playground/PlaygroundButton.tsx +++ b/packages/ui/app/src/playground/PlaygroundButton.tsx @@ -3,13 +3,13 @@ import { FernButton, FernTooltip, FernTooltipProvider } from "@fern-ui/component import { PlaySolid } from "iconoir-react"; import { useAtomValue } from "jotai"; import { FC } from "react"; -import { IS_PLAYGROUND_ENABLED_ATOM, useSetAndOpenPlayground } from "../atoms"; +import { IS_PLAYGROUND_ENABLED_ATOM, useOpenPlayground } from "../atoms"; import { usePlaygroundSettings } from "../hooks/usePlaygroundSettings"; export const PlaygroundButton: FC<{ state: FernNavigation.NavigationNodeApiLeaf; }> = ({ state }) => { - const openPlayground = useSetAndOpenPlayground(); + const openPlayground = useOpenPlayground(); const isPlaygroundEnabled = useAtomValue(IS_PLAYGROUND_ENABLED_ATOM); const settings = usePlaygroundSettings(state.id); diff --git a/packages/ui/app/src/playground/endpoint/PlaygroundEndpointSelectorLeafNode.tsx b/packages/ui/app/src/playground/endpoint/PlaygroundEndpointSelectorLeafNode.tsx index 4f95a019ac..3ffbec1122 100644 --- a/packages/ui/app/src/playground/endpoint/PlaygroundEndpointSelectorLeafNode.tsx +++ b/packages/ui/app/src/playground/endpoint/PlaygroundEndpointSelectorLeafNode.tsx @@ -3,14 +3,12 @@ import { FernButton, FernTooltip } from "@fern-ui/components"; import { HttpMethodBadge } from "@fern-ui/components/badges"; import clsx from "clsx"; import { atom, useAtomValue } from "jotai"; -import dynamic from "next/dynamic"; import { ReactElement, forwardRef } from "react"; import { useMemoOne } from "use-memo-one"; -import { getApiDefinitionAtom, useSetAndOpenPlayground } from "../../atoms"; +import { getApiDefinitionAtom, useOpenPlayground } from "../../atoms"; +import { Markdown } from "../../mdx/Markdown"; import { usePreloadApiLeaf } from "../hooks/usePreloadApiLeaf"; -const Markdown = dynamic(() => import("../../mdx/Markdown").then(({ Markdown }) => Markdown)); - interface PlaygroundEndpointSelectorLeafNodeProps { node: FernNavigation.EndpointNode | FernNavigation.WebSocketNode; filterValue: string; @@ -42,7 +40,7 @@ export const PlaygroundEndpointSelectorLeafNode = forwardRef () => { void setSelectionStateAndOpen(endpoint); diff --git a/packages/ui/app/src/search/SearchV2.tsx b/packages/ui/app/src/search/SearchV2.tsx index bb277ceded..20510347c3 100644 --- a/packages/ui/app/src/search/SearchV2.tsx +++ b/packages/ui/app/src/search/SearchV2.tsx @@ -2,6 +2,7 @@ import { CommandActions, CommandEmpty, CommandGroupFilters, + CommandGroupPlayground, CommandGroupTheme, CommandSearchHits, DesktopBackButton, @@ -26,10 +27,13 @@ import { z } from "zod"; import { CURRENT_VERSION_ATOM, DOMAIN_ATOM, + HAS_API_PLAYGROUND, THEME_SWITCH_ENABLED_ATOM, atomWithStorageString, useFernUser, + useIsPlaygroundOpen, useSetTheme, + useTogglePlayground, } from "../atoms"; import { useApiRoute } from "../hooks/useApiRoute"; import { useApiRouteSWRImmutable } from "../hooks/useApiRouteSWR"; @@ -56,8 +60,6 @@ export function SearchV2(): ReactElement | false { const [open, setOpen] = useCommandTrigger(); const domain = useAtomValue(DOMAIN_ATOM); - const isThemeSwitchEnabled = useAtomValue(THEME_SWITCH_ENABLED_ATOM); - const setTheme = useSetTheme(); const { data } = useApiRouteSWRImmutable("/api/fern-docs/search/v2/key", { request: { headers: { "X-User-Token": userToken } }, @@ -112,13 +114,51 @@ export function SearchV2(): ReactElement | false { setOpen(false)} /> - {isThemeSwitchEnabled && } + + setOpen(false)} /> + setOpen(false)} /> + ); } +function CommandPlayground({ onClose }: { onClose: () => void }) { + const hasApiPlayground = useAtomValue(HAS_API_PLAYGROUND); + const togglePlayground = useTogglePlayground(); + const playgroundOpen = useIsPlaygroundOpen(); + + if (!hasApiPlayground) { + return null; + } + return ( + { + togglePlayground(); + onClose(); + }} + playgroundOpen={playgroundOpen} + /> + ); +} + +function CommandTheme({ onClose }: { onClose: () => void }) { + const isThemeSwitchEnabled = useAtomValue(THEME_SWITCH_ENABLED_ATOM); + const setTheme = useSetTheme(); + if (!isThemeSwitchEnabled) { + return null; + } + return ( + { + setTheme(theme); + onClose(); + }} + /> + ); +} + function RouterAwareCommandSearchHits({ onClose }: { onClose: () => void }) { const router = useRouter(); diff --git a/packages/ui/fern-docs-search-ui/src/components/shared/command-playground.tsx b/packages/ui/fern-docs-search-ui/src/components/shared/command-playground.tsx new file mode 100644 index 0000000000..4ab4b1f653 --- /dev/null +++ b/packages/ui/fern-docs-search-ui/src/components/shared/command-playground.tsx @@ -0,0 +1,31 @@ +import { Kbd } from "@fern-ui/components"; +import { Command } from "cmdk"; +import { Play } from "lucide-react"; +import { ComponentPropsWithoutRef, forwardRef } from "react"; + +export const CommandGroupPlayground = forwardRef< + HTMLDivElement, + ComponentPropsWithoutRef & { + togglePlayground?: () => void; + playgroundOpen?: boolean; + } +>(({ togglePlayground, playgroundOpen, ...props }, ref) => { + if (togglePlayground == null) { + return false; + } + + return ( + + togglePlayground()} + > + + {playgroundOpen ? "Close API Playground" : "Open API Playground"} + ctrl+` + + + ); +}); + +CommandGroupPlayground.displayName = "CommandGroupPlayground"; diff --git a/packages/ui/fern-docs-search-ui/src/components/shared/index.ts b/packages/ui/fern-docs-search-ui/src/components/shared/index.ts index 38163972be..5e3ff76eb4 100644 --- a/packages/ui/fern-docs-search-ui/src/components/shared/index.ts +++ b/packages/ui/fern-docs-search-ui/src/components/shared/index.ts @@ -2,5 +2,6 @@ export * from "./command-actions"; export * from "./command-empty"; export * from "./command-filters"; export * from "./command-hits"; +export * from "./command-playground"; export * from "./command-theme"; export * from "./command-ux";