Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
nodejs 20.19.3
nodejs 24.13.0
yarn 1.22.10
127 changes: 127 additions & 0 deletions components/api-reference/ApiReferenceLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { useRef } from "react";
import { useRouter } from "next/router";
import Meta from "@/components/Meta";
import { Page as TelegraphPage } from "@/components/ui/Page";
import { Sidebar, SidebarContext } from "@/components/ui/Page/Sidebar";
import { ContentActions } from "@/components/ui/ContentActions";
import { SidebarData, SidebarSection } from "@/lib/openApiSpec";
import { SidebarSection as LegacySidebarSection } from "@/data/types";

interface Breadcrumb {
label: string;
href: string;
}

interface ApiReferenceLayoutProps {
children: React.ReactNode;
sidebarData: SidebarData;
preSidebarContent?: LegacySidebarSection[];
title: string;
description: string;
breadcrumbs?: Breadcrumb[];
currentPath?: string;
}

/**
* Convert new SidebarData format to legacy SidebarSection format
* used by the existing Page components.
*/
function convertToLegacySidebarFormat(
sidebarData: SidebarData,
preSidebarContent: LegacySidebarSection[] = [],
): LegacySidebarSection[] {
const resourceSections: LegacySidebarSection[] = sidebarData.resources.map(
(resource: SidebarSection) => ({
title: resource.title,
slug: resource.slug,
pages: [
{ slug: "/", title: "Overview" },
...resource.pages.map((page) => ({
slug: page.slug.replace(resource.slug, ""),
title: page.title,
// subPage slugs need to be relative to their parent page, not the resource
pages: page.pages?.map((subPage) => ({
slug: subPage.slug.replace(page.slug, ""),
title: subPage.title,
})),
})),
],
}),
);

return [...preSidebarContent, ...resourceSections];
}

export function ApiReferenceLayout({
children,
sidebarData,
preSidebarContent = [],
title,
description,
breadcrumbs,
currentPath,
}: ApiReferenceLayoutProps) {
const router = useRouter();
const scrollerRef = useRef<HTMLDivElement>(null);

const basePath = router.pathname.split("/")[1];
const canonicalPath = currentPath || `/${basePath}`;

const sidebarContent = convertToLegacySidebarFormat(
sidebarData,
preSidebarContent,
);

// For per-resource pages, currentPath is the resource base path (e.g., /api-reference/users)
// This enables same-page routing for links within the current resource
const sidebarContextValue = {
samePageRouting: true,
currentResourcePath: currentPath,
};

return (
<TelegraphPage.Container>
<Meta
title={`${title} | Knock Docs`}
description={description}
canonical={canonicalPath}
/>
<TelegraphPage.Masthead
mobileSidebar={
<SidebarContext.Provider value={sidebarContextValue}>
<TelegraphPage.MobileSidebar
samePageRouting
content={sidebarContent}
/>
</SidebarContext.Provider>
}
/>
<TelegraphPage.Wrapper>
<SidebarContext.Provider value={sidebarContextValue}>
<Sidebar.FullLayout scrollerRef={scrollerRef}>
<Sidebar.ScrollContainer scrollerRef={scrollerRef}>
{sidebarContent.map((section) => (
<Sidebar.Section key={section.slug} section={section} />
))}
</Sidebar.ScrollContainer>
</Sidebar.FullLayout>
</SidebarContext.Provider>
<TelegraphPage.Content>
{breadcrumbs && breadcrumbs.length > 0 && (
<TelegraphPage.Breadcrumbs items={breadcrumbs} />
)}
<TelegraphPage.ContentHeader
title={title}
description={description}
bottomContent={
<ContentActions showOnMobile style={{ marginLeft: "-6px" }} />
}
/>
<TelegraphPage.ContentBody>{children}</TelegraphPage.ContentBody>
</TelegraphPage.Content>
</TelegraphPage.Wrapper>
</TelegraphPage.Container>
);
}

export default ApiReferenceLayout;
31 changes: 31 additions & 0 deletions components/api-reference/ResourceFullPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ApiReferenceProvider } from "@/components/ui/ApiReference/ApiReferenceContext";
import { ApiReferenceSection } from "@/components/ui/ApiReference";
import { FullResourcePageData } from "@/lib/openApiSpec";
import { useInitialScrollState } from "@/components/ui/Page/helpers";

interface ResourceFullPageProps {
data: FullResourcePageData;
basePath: string;
}

/**
* Renders a full resource page with all methods, schemas, and subresources.
* This component wraps the existing ApiReferenceSection with the proper context.
*/
export function ResourceFullPage({ data, basePath }: ResourceFullPageProps) {
// Handle scroll to hash on initial load
useInitialScrollState();

return (
<ApiReferenceProvider
openApiSpec={data.openApiSpec}
stainlessConfig={data.stainlessConfig}
>
<ApiReferenceSection
resourceName={data.resourceName}
resource={data.resource}
path={`/${data.resourceName}`}
/>
</ApiReferenceProvider>
);
}
2 changes: 2 additions & 0 deletions components/api-reference/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ApiReferenceLayout } from "./ApiReferenceLayout";
export { ResourceFullPage } from "./ResourceFullPage";
9 changes: 4 additions & 5 deletions components/ui/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ChevronRight } from "lucide-react";

const AccordionGroup = ({ children }) => (
<div
className="[&>div]:border-0 [&>div]:rounded-none [&>div>button]:rounded-none [&>div]:mb-0 overflow-hidden mt-0 mb-3 rounded-xl divide-y divide-inherit border dark:border-zinc-800"
className="[&>div]:border-0 [&>div]:rounded-none [&>div>button]:rounded-none [&>div]:mb-0 overflow-hidden mt-0 mb-6 rounded-xl divide-y divide-inherit border dark:border-zinc-800"
role="list"
>
{children}
Expand Down Expand Up @@ -37,8 +37,7 @@ const Accordion = ({
onClick={() => setOpen(!open)}
aria-controls={title + "Children"}
aria-expanded={open}
py="8"
px="8"
p="6"
w="full"
justifyContent="flex-start"
alignItems="center"
Expand All @@ -54,10 +53,10 @@ const Accordion = ({
flexShrink: 0,
}}
/>
<Box ml="2">
<Box>
<Text
as="span"
size="3"
size="2"
leading="2"
weight="medium"
// eslint-disable-next-line
Expand Down
56 changes: 56 additions & 0 deletions components/ui/ApiReference/ApiReferenceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ const ApiReferenceContext = createContext<ApiReferenceContextType | undefined>(
undefined,
);

/**
* Lightweight context that only provides schemaReferences and baseUrl.
* Used by multi-page API reference components.
*/
interface LightweightContextType {
schemaReferences: Record<string, string>;
baseUrl: string;
}

const LightweightContext = createContext<LightweightContextType | undefined>(
undefined,
);

interface ApiReferenceProviderProps {
children: ReactNode;
openApiSpec: OpenAPIV3.Document;
Expand Down Expand Up @@ -46,6 +59,28 @@ export function ApiReferenceProvider({
);
}

/**
* Lightweight provider for multi-page API reference that only needs
* schemaReferences and baseUrl (without loading full specs).
*/
interface LightweightApiReferenceProviderProps {
children: ReactNode;
schemaReferences: Record<string, string>;
baseUrl: string;
}

export function LightweightApiReferenceProvider({
children,
schemaReferences,
baseUrl,
}: LightweightApiReferenceProviderProps) {
return (
<LightweightContext.Provider value={{ baseUrl, schemaReferences }}>
{children}
</LightweightContext.Provider>
);
}

export function useApiReference() {
const context = useContext(ApiReferenceContext);
if (context === undefined) {
Expand All @@ -56,4 +91,25 @@ export function useApiReference() {
return context;
}

/**
* Hook that returns schemaReferences from either the full or lightweight context.
* Use this in components that only need schemaReferences.
*/
export function useSchemaReferences(): Record<string, string> {
const fullContext = useContext(ApiReferenceContext);
const lightweightContext = useContext(LightweightContext);

if (fullContext) {
return fullContext.schemaReferences;
}

if (lightweightContext) {
return lightweightContext.schemaReferences;
}

throw new Error(
"useSchemaReferences must be used within an ApiReferenceProvider or LightweightApiReferenceProvider",
);
}

export default ApiReferenceContext;
Loading
Loading