Skip to content
Merged
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
12 changes: 7 additions & 5 deletions apps/docs/app/api/(guides)/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,13 @@ export default async function ApiGuidePage({ params }: { params: Promise<{ slug:
__html: JSON.stringify(jsonLd).replace(/</g, '\\u003c'),
}}
/>
<StaticPageHeader
title={data.frontmatter.title}
pageUrl={pageUrl}
sectionTitle={getSectionTitle(pageUrl) || undefined}
/>
{!data.frontmatter.hideHeader && (
<StaticPageHeader
title={data.frontmatter.title}
pageUrl={pageUrl}
sectionTitle={getSectionTitle(pageUrl) || undefined}
/>
)}
<MarkdownContent content={data.content} />
</StaticPageWrapper>
);
Expand Down
126 changes: 68 additions & 58 deletions apps/docs/components/api/code-examples.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
'use client';

import { useState, useMemo, useEffect } from 'react';
import { useMemo } from 'react';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { ChevronDown } from 'lucide-react';
import type { Endpoint } from '@/lib/openapi/types';
import { generateCurl } from '@/lib/code-generators/curl';
import { generateCLI } from '@/lib/code-generators/cli';
import { generateResponseExample, type ExampleOptions } from '@/lib/openapi/example-generator';
import { useCodeLang } from '@/lib/use-code-lang';
import { CopyButton } from './copy-button';
import { CodeBlock } from './code-block';
import { BoxedPanel } from './boxed-panel';
import { LangTabs } from './lang-tabs';

interface CodeExamplesProps {
endpoint: Endpoint;
Expand All @@ -20,13 +22,13 @@ interface CodeExamplesProps {
sdkExamples?: Record<string, string>;
langs?: Language[];
defaultLang?: Language;
/** Language switcher style in the request header: dropdown (default) or inline tabs. */
variant?: 'dropdown' | 'tabs';
className?: string;
}

type Language = 'curl' | 'cli' | 'typescript' | 'python' | 'go' | 'kotlin' | 'swift' | 'csharp';

const STORAGE_KEY = 'pachca-docs-code-lang';

const languageLabels: Record<Language, string> = {
cli: 'Pachca CLI',
curl: 'cURL',
Expand All @@ -47,26 +49,20 @@ export function CodeExamples({
sdkExamples,
langs,
defaultLang: defaultLangProp,
variant = 'dropdown',
className,
}: CodeExamplesProps) {
const allLangs = Object.keys(languageLabels) as Language[];
const visibleLangs = langs ?? allLangs;
const fallbackLang = visibleLangs[0];

const [activeTab, setActiveTab] = useState<Language>(defaultLangProp ?? fallbackLang);

useEffect(() => {
if (defaultLangProp) return;
const saved = localStorage.getItem(STORAGE_KEY);
if (saved && saved in languageLabels && visibleLangs.includes(saved as Language)) {
setActiveTab(saved as Language); // eslint-disable-line react-hooks/set-state-in-effect
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const [storedLang, setStoredLang] = useCodeLang(fallbackLang, defaultLangProp);
// The shared value may be a language this instance doesn't show — fall back then.
const activeTab: Language = visibleLangs.includes(storedLang as Language)
? (storedLang as Language)
: fallbackLang;

const handleTabChange = (lang: Language) => {
setActiveTab(lang);
localStorage.setItem(STORAGE_KEY, lang);
};
const handleTabChange = (lang: Language) => setStoredLang(lang);

const code = useMemo(() => {
if (activeTab === 'curl') return generateCurl(endpoint, baseUrl);
Expand Down Expand Up @@ -124,49 +120,63 @@ export function CodeExamples({
id="request-examples"
className={showResponse ? 'mt-0 mb-6' : (className ?? 'my-0')}
header={
<>
<span className="text-[13px] font-medium text-text-primary truncate mr-4">
{title || endpoint.title || endpoint.summary || endpoint.path}
</span>

<div className="flex items-center gap-0 shrink-0">
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button className="flex items-center gap-1 px-2 h-7 rounded-md text-[13px] font-medium text-text-secondary hover:text-text-primary transition-all outline-none focus:outline-none focus:ring-0 focus-visible:ring-0 select-none cursor-pointer group">
{languages[activeTab]}
<ChevronDown
className="w-3.5 h-3.5 text-text-secondary group-hover:text-text-primary transition-colors"
strokeWidth={2.5}
/>
</button>
</DropdownMenu.Trigger>

<DropdownMenu.Portal>
<DropdownMenu.Content
className="z-50 min-w-[140px] bg-glass-heavy backdrop-blur-xl border border-glass-heavy-border rounded-xl p-1.5 space-y-0.5 shadow-xl animate-dropdown"
align="end"
collisionPadding={16}
>
{(Object.keys(languages) as Language[]).map((lang) => (
<DropdownMenu.Item
key={lang}
onClick={() => handleTabChange(lang)}
className={`flex items-center px-2.5 py-1.5 text-[13px] font-medium rounded-md cursor-pointer outline-none transition-colors ${
activeTab === lang
? 'bg-primary/15 text-primary'
: 'text-text-primary hover:bg-glass-hover'
}`}
>
{languages[lang]}
</DropdownMenu.Item>
))}
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>

variant === 'tabs' ? (
<>
<LangTabs
items={(Object.keys(languages) as Language[]).map((l) => ({
id: l,
label: languages[l],
}))}
activeId={activeTab}
onSelect={(id) => handleTabChange(id as Language)}
/>
<CopyButton text={code} />
</div>
</>
</>
) : (
<>
<span className="text-[13px] font-medium text-text-primary truncate mr-4">
{title || endpoint.title || endpoint.summary || endpoint.path}
</span>

<div className="flex items-center gap-0 shrink-0">
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button className="flex items-center gap-1 px-2 h-7 rounded-md text-[13px] font-medium text-text-secondary hover:text-text-primary transition-all outline-none focus:outline-none focus:ring-0 focus-visible:ring-0 select-none cursor-pointer group">
{languages[activeTab]}
<ChevronDown
className="w-3.5 h-3.5 text-text-secondary group-hover:text-text-primary transition-colors"
strokeWidth={2.5}
/>
</button>
</DropdownMenu.Trigger>

<DropdownMenu.Portal>
<DropdownMenu.Content
className="z-50 min-w-[140px] bg-glass-heavy backdrop-blur-xl border border-glass-heavy-border rounded-xl p-1.5 space-y-0.5 shadow-xl animate-dropdown"
align="end"
collisionPadding={16}
>
{(Object.keys(languages) as Language[]).map((lang) => (
<DropdownMenu.Item
key={lang}
onClick={() => handleTabChange(lang)}
className={`flex items-center px-2.5 py-1.5 text-[13px] font-medium rounded-md cursor-pointer outline-none transition-colors ${
activeTab === lang
? 'bg-primary/15 text-primary'
: 'text-text-primary hover:bg-glass-hover'
}`}
>
{languages[lang]}
</DropdownMenu.Item>
))}
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>

<CopyButton text={code} />
</div>
</>
)
}
contentClassName="px-6 py-2 pl-0 overflow-x-auto custom-scrollbar"
>
Expand Down
47 changes: 47 additions & 0 deletions apps/docs/components/api/lang-tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client';

export interface LangTabItem {
id: string;
label: string;
}

/**
* Inline language/tool switcher rendered as tabs, styled like the header nav:
* normal text color with a thin primary underline on the active tab. Sits in a
* row with a `border-b`; the tab height matches `--boxed-header-height` so the
* underline lands on the divider both in a BoxedPanel header and a plain row.
*
* Shared by <ApiClientPanel> and <CodeExamples variant="tabs">.
*/
export function LangTabs({
items,
activeId,
onSelect,
className,
}: {
items: LangTabItem[];
activeId: string | undefined;
onSelect: (id: string) => void;
className?: string;
}) {
return (
<div className={`flex gap-x-5 self-stretch ${className ?? ''}`}>
{items.map((it) => {
const isActive = it.id === activeId;
return (
<button
key={it.id}
type="button"
onClick={() => onSelect(it.id)}
className={`relative py-2.5 text-[13px] font-medium whitespace-nowrap transition-colors duration-200 cursor-pointer select-none ${
isActive ? 'text-text-primary' : 'text-text-secondary hover:text-text-primary'
}`}
>
{it.label}
{isActive && <span className="absolute left-0 right-0 bottom-0 h-px bg-primary" />}
</button>
);
})}
</div>
);
}
4 changes: 4 additions & 0 deletions apps/docs/components/api/markdown-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import { SdkCommands } from '@/components/mdx/sdk-commands';
import { NpmBadge } from '@/components/mdx/npm-badge';
import { PackageBadge } from '@/components/mdx/package-badge';
import { HomeHero, HomeHeroContent, HomeHeroCode } from '@/components/mdx/home-hero';
import { ApiClientPanel } from '@/components/mdx/api-client-panel';
import { ApiIntroNotes } from '@/components/mdx/api-intro-notes';
import { Tabs, Tab } from '@/components/mdx/tabs';
import { WebhookPlayground } from '@/components/mdx/webhook-playground';
import { MessagePlayground } from '@/components/mdx/message-playground';
Expand Down Expand Up @@ -239,6 +241,8 @@ const components = {
HomeHero,
HomeHeroContent,
HomeHeroCode,
ApiClientPanel,
ApiIntroNotes,
Tabs,
Tab,
WebhookPlayground,
Expand Down
66 changes: 66 additions & 0 deletions apps/docs/components/mdx/api-client-panel-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use client';

import Link from 'next/link';
import { GuideCodeBlock } from '@/components/api/guide-code-block';
import { CodeBlock } from '@/components/api/code-block';
import { CopyButton } from '@/components/api/copy-button';
import { LangTabs } from '@/components/api/lang-tabs';
import { useCodeLang } from '@/lib/use-code-lang';
import type { ApiClient } from '@/lib/api-clients';

export function ApiClientPanelClient({
baseUrl,
clients,
}: {
baseUrl: string;
clients: ApiClient[];
}) {
const [lang, setLang] = useCodeLang(clients[0]?.id ?? 'cli');
// The shared value may be a language without an install entry (e.g. cURL) — fall back then.
const active = clients.find((c) => c.id === lang) ?? clients[0];

return (
<div className="not-prose flex flex-col gap-4">
{/* Base URL — standard titled code block */}
<GuideCodeBlock title="Базовый URL" code={baseUrl} language="text" className="m-0" />

{/* Client libraries — tabs header (scrolls on narrow), code below the divider */}
<div className="flex flex-col gap-2">
<div className="rounded-xl border border-glass-border bg-glass overflow-hidden">
{/* Tab bar: scrolls on narrow; copy button pinned right (blur like a code block) */}
<div className="relative border-b border-glass-border">
<div className="overflow-x-auto overflow-y-hidden custom-scrollbar">
<LangTabs
items={clients.map((c) => ({ id: c.id, label: c.short ?? c.label }))}
activeId={active?.id}
onSelect={setLang}
className="w-max pl-4 pr-12"
/>
</div>
{active && (
<div className="absolute top-0 right-0 z-10 rounded-bl-xl rounded-tl-xl pt-2 pb-2 pl-3 pr-3 backdrop-blur-sm">
<CopyButton text={active.install} />
</div>
)}
</div>
{/* Active install command */}
<div className="px-4 py-2.5 overflow-x-auto custom-scrollbar">
{active && <CodeBlock code={active.install} language={active.lang} />}
</div>
</div>

{active && (
<div className="px-1 text-[13px] leading-relaxed text-text-tertiary">
{active.blurb} в{' '}
<Link
href={active.href}
className="endpoint-link font-medium text-text-tertiary underline underline-offset-2 hover:text-text-secondary transition-colors"
>
документации {active.label}
</Link>
</div>
)}
</div>
</div>
);
}
29 changes: 29 additions & 0 deletions apps/docs/components/mdx/api-client-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { getBaseUrl } from '@/lib/openapi/parser';
import { API_CLIENTS } from '@/lib/api-clients';
import { Card } from '@/components/mdx/cards';
import { ApiClientPanelClient } from './api-client-panel-client';

/** Downloadable specs and files, shown below the client panel. */
const RESOURCES = [
{ title: 'OpenAPI', icon: 'FileSearch', href: '/openapi.yaml' },
{ title: 'Postman', icon: 'LayoutList', href: '/pachca.postman_collection.json' },
];

/**
* Right-column panel for /api/overview: the base URL (read from the spec), a
* tabbed switcher over the official clients (CLI and the six SDKs), and
* downloadable resources (spec, Postman, llms.txt).
*/
export async function ApiClientPanel() {
const baseUrl = await getBaseUrl();
return (
<div className="not-prose flex flex-col gap-4">
<ApiClientPanelClient baseUrl={baseUrl} clients={API_CLIENTS} />
<div className="flex flex-wrap gap-2.5">
{RESOURCES.map((r) => (
<Card key={r.href} compact title={r.title} icon={r.icon} href={r.href} download />
))}
</div>
</div>
);
}
22 changes: 22 additions & 0 deletions apps/docs/components/mdx/api-intro-notes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Link from 'next/link';

/**
* Extra intro paragraphs for the /api/overview left column (passed to
* <HomeHeroContent lead>): getting-started and no-code pointers as plain
* prose, no headings.
*/
export function ApiIntroNotes() {
return (
<p>
Если только начинаете, загляните в{' '}
<Link href="/" className="text-primary hover:underline">
руководства
</Link>{' '}
с пошаговыми примерами. Чтобы автоматизировать Пачку без кода, используйте{' '}
<Link href="/guides/n8n/overview" className="text-primary hover:underline">
n8n
</Link>{' '}
и визуальные сценарии.
</p>
);
}
Loading
Loading