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
152 changes: 152 additions & 0 deletions packages/landing/src/components/docs/endpoint-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
'use client';

// Interactive, grouped endpoint accordion for /docs/rest-api. Reproduces the
// approved prototype (_archive/proto/api-reference-page) against the landing's
// semantic tokens. Rows collapse via a CSS toggle so the contract stays in the
// prerendered HTML; each row opens independently.

import * as React from 'react';
import { ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import { CodeBlock } from '@/docs/components/ui';
import { JsonBlock } from './json-highlight';
import type { Endpoint, EndpointArg, EndpointGroup, HttpMethod } from '@/docs/types';

const METHOD_CLASS: Record<HttpMethod, string> = {
GET: 'text-success bg-success/10 border-success/20',
POST: 'text-info bg-info/10 border-info/20',
PUT: 'text-warning bg-warning/10 border-warning/20',
PATCH: 'text-tag-schedules bg-tag-schedules/10 border-tag-schedules/20',
DELETE: 'text-destructive bg-destructive/10 border-destructive/20',
};

function SectionLabel({ children }: { children: React.ReactNode }): React.JSX.Element {
return (
<p className="mt-4 mb-1.5 text-[11px] font-semibold tracking-[0.08em] text-muted-foreground uppercase">
{children}
</p>
);
}

function ArgTable({ rows }: { rows: EndpointArg[] }): React.JSX.Element {
return (
<table className="w-full border-collapse">
<tbody>
{rows.map((r) => (
<tr key={r.name}>
<td className="py-1.5 pr-4 align-top font-mono text-[12px] whitespace-nowrap text-foreground">
{r.name}
</td>
<td className="py-1.5 pr-4 align-top font-mono text-[11px] whitespace-nowrap text-muted-foreground">
{r.type}
</td>
<td className="w-full py-1.5 align-top text-[12.5px] text-muted-foreground">
{r.description}
</td>
</tr>
))}
</tbody>
</table>
);
}

function EndpointRow({ endpoint }: { endpoint: Endpoint }): React.JSX.Element {
const [open, setOpen] = React.useState(false);
const bodyId = React.useId();
const planned = endpoint.status === 'planned';
return (
<article className="overflow-hidden rounded-lg border border-border bg-card">
<button
type="button"
aria-expanded={open}
aria-controls={bodyId}
onClick={() => setOpen((v) => !v)}
className="flex w-full cursor-pointer items-center gap-3 px-4 py-3 text-left hover:bg-accent/40 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-inset"
>
<span
className={cn(
'inline-grid w-16 place-items-center rounded-md border py-1 font-mono text-[11px] font-bold tracking-wide',
METHOD_CLASS[endpoint.method]
)}
>
{endpoint.method}
</span>
<span className="font-mono text-[13px] text-foreground">{endpoint.path}</span>
<span className="hidden flex-1 truncate text-[12.5px] text-muted-foreground sm:inline">
{endpoint.summary}
</span>
<span
className={cn(
'inline-flex items-center gap-1.5 rounded-full border px-2 py-1 text-[10.5px] leading-none whitespace-nowrap',
planned
? 'border-border text-muted-foreground'
: 'border-primary/25 bg-primary-soft text-primary'
)}
>
<span className="size-1.5 rounded-full bg-current" />
{planned ? `Planned · wave ${endpoint.wave}` : 'Live'}
</span>
<ChevronRight
aria-hidden
className={cn(
'size-3.5 shrink-0 text-muted-foreground transition-transform duration-150',
open && 'rotate-90'
)}
/>
</button>
<div id={bodyId} className={cn('border-t border-border px-4 py-4', !open && 'hidden')}>
<p className="max-w-[640px] text-[13px] leading-relaxed text-muted-foreground">
{endpoint.description}
</p>
{planned && (
<p className="mt-2 text-[12px] text-warning">
Draft contract from the north-star spec. Not callable yet.
</p>
)}
{endpoint.params && (
<>
<SectionLabel>Parameters</SectionLabel>
<ArgTable rows={endpoint.params} />
</>
)}
<SectionLabel>Example</SectionLabel>
<CodeBlock code={endpoint.curl} language="bash" />
<SectionLabel>Response 200</SectionLabel>
<JsonBlock code={endpoint.response} />
<SectionLabel>Fields</SectionLabel>
<ArgTable rows={endpoint.fields} />
<SectionLabel>Errors</SectionLabel>
<p className="text-[12.5px] text-muted-foreground">
{endpoint.errors.map((e, i) => (
<React.Fragment key={e.status}>
{i > 0 && <span> · </span>}
<span className="rounded border border-border bg-background px-1.5 py-0.5 font-mono text-[11.5px]">
{e.status}
</span>{' '}
{e.meaning}
</React.Fragment>
))}
</p>
</div>
</article>
);
}

export function EndpointList({ groups }: { groups: EndpointGroup[] }): React.JSX.Element {
return (
<div className="my-4">
{groups.map((g) => (
<section key={g.group} className="mb-8 last:mb-0">
<h3 className="mb-3 text-[13px] font-semibold tracking-[0.08em] text-muted-foreground uppercase">
{g.group}
</h3>
<div className="space-y-2">
{g.items.map((e) => (
<EndpointRow key={`${e.method} ${e.path}`} endpoint={e} />
))}
</div>
</section>
))}
</div>
);
}
95 changes: 95 additions & 0 deletions packages/landing/src/components/docs/json-highlight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use client';

// Structured, syntax-highlighted JSON for the endpoint response blocks. We avoid
// pulling shiki into this client island (async + heavy); a tiny tokenizer colors
// keys/strings/numbers/booleans/null via the landing's semantic tokens, so it
// works in both light and dark themes. Responses are normalized through
// JSON.parse/stringify (2-space) - nothing is hand-aligned.

import * as React from 'react';

interface Piece {
text: string;
className?: string;
}

// Alternation: quoted string (optionally a key when followed by a colon),
// literal keyword, or number.
const TOKEN =
/("(?:\\.|[^"\\])*")(\s*:)?|\b(true|false|null)\b|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g;

function tokenize(json: string): Piece[] {
const pieces: Piece[] = [];
let last = 0;
TOKEN.lastIndex = 0;
let m: RegExpExecArray | null;
while ((m = TOKEN.exec(json)) !== null) {
if (m.index > last) pieces.push({ text: json.slice(last, m.index) });
const [full, str, colon, keyword] = m;
if (str !== undefined) {
if (colon !== undefined) {
pieces.push({ text: str, className: 'text-info' });
pieces.push({ text: colon });
} else {
pieces.push({ text: str, className: 'text-success' });
}
} else if (keyword !== undefined) {
pieces.push({ text: full, className: 'text-tag-schedules' });
} else {
pieces.push({ text: full, className: 'text-warning' });
}
last = m.index + full.length;
}
if (last < json.length) pieces.push({ text: json.slice(last) });
return pieces;
}

function normalize(code: string): { text: string; highlighted: boolean } {
try {
return { text: JSON.stringify(JSON.parse(code), null, 2), highlighted: true };
} catch {
return { text: code, highlighted: false };
}
}

export function JsonBlock({ code }: { code: string }): React.JSX.Element {
const [copied, setCopied] = React.useState(false);
const { text, highlighted } = normalize(code);
const copy = async (): Promise<void> => {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
return (
<div className="overflow-hidden rounded-lg border border-border bg-card">
<div className="flex items-center justify-between border-b border-border px-4 py-2">
<span className="font-mono text-xs text-muted-foreground">
{highlighted ? 'json' : 'text'}
</span>
<button
type="button"
onClick={() => void copy()}
className="rounded px-2 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
aria-label={copied ? 'Copied' : 'Copy code'}
>
{copied ? 'Copied' : 'Copy'}
</button>
</div>
<pre className="overflow-x-auto p-4 text-[13.5px] leading-relaxed">
<code className="font-mono text-foreground">
{highlighted
? tokenize(text).map((p, i) =>
p.className ? (
<span key={i} className={p.className}>
{p.text}
</span>
) : (
<React.Fragment key={i}>{p.text}</React.Fragment>
)
)
: text}
</code>
</pre>
</div>
);
}
4 changes: 4 additions & 0 deletions packages/landing/src/docs/components/doc-article.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { Info, TriangleAlert, CircleCheck } from 'lucide-react';
import { Alert, CodeBlock, Md, Tabs, TabsContent, TabsList, TabsTrigger } from './ui';
import { EndpointList } from '@/components/docs/endpoint-list';
import type { CalloutVariant, DocBlock, DocPage } from '../types';

const calloutMeta: Record<
Expand Down Expand Up @@ -77,6 +78,9 @@ function Block({ block }: { block: DocBlock }): React.JSX.Element {
);
}

case 'endpoints':
return <EndpointList groups={block.groups} />;

case 'steps':
return (
<ol className="my-4 space-y-4">
Expand Down
97 changes: 9 additions & 88 deletions packages/landing/src/docs/content/rest-api.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { DocPage } from '../types';
import { MCP_AUTH_ORIGIN, REST_RATE_LIMIT_PER_MIN, REST_VAULTS_ENDPOINT } from '@/lib/mcp-docs';
import { API_ENDPOINTS } from '@/lib/api-endpoints';

// REST API reference - one read-only endpoint that lists the vaults a token can
// see. Facts (base URL, endpoint, rate limit) come from lib/mcp-docs.ts.
// REST API reference. One live endpoint (GET /v1/vaults) plus the north-star
// draft contracts, rendered as an interactive list from lib/api-endpoints.ts.
// Facts (base URL, endpoint, rate limit) come from lib/mcp-docs.ts.
export const restApiDoc: DocPage = {
slug: 'rest-api',
title: 'REST API',
Expand Down Expand Up @@ -51,97 +53,16 @@ export const restApiDoc: DocPage = {
],
},
{
id: 'example',
title: 'Example',
blocks: [
{
type: 'code',
language: 'bash',
code: `curl -s \\
-H "Authorization: Bearer $AGENTAGE_TOKEN" \\
${REST_VAULTS_ENDPOINT}`,
},
{
type: 'code',
language: 'json',
code: `{
"vaults": [
{
"name": "default",
"files": 412,
"folders": 37,
"updated": "2026-07-06T07:31:02+00:00",
"empty": false
},
{
"name": "work",
"files": 128,
"folders": 12,
"updated": "2026-07-05T21:47:55+00:00",
"empty": false
}
]
}`,
},
],
},
{
id: 'response-schema',
title: 'Response schema',
id: 'endpoints',
title: 'Endpoints',
blocks: [
{
type: 'p',
md: [
'| Field | Type | Meaning |',
'| --- | --- | --- |',
'| `vaults` | array | Vaults visible to the presented token, one object per vault. |',
'| `vaults[].name` | string | Vault slug, 1-64 chars of `a-z 0-9 _ -`. |',
'| `vaults[].files` | integer | Number of notes currently in the vault. |',
'| `vaults[].folders` | integer | Number of folders in the vault. |',
'| `vaults[].updated` | string (ISO 8601) or null | Last write to the vault; `null` when the vault has no content yet. |',
'| `vaults[].empty` | boolean | True when the vault has no notes. |',
].join('\n'),
},
{
type: 'code',
language: 'json',
code: `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["vaults"],
"properties": {
"vaults": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "files", "folders", "updated", "empty"],
"properties": {
"name": { "type": "string", "pattern": "^[a-z0-9_-]{1,64}$" },
"files": { "type": "integer", "minimum": 0 },
"folders": { "type": "integer", "minimum": 0 },
"updated": { "type": ["string", "null"], "format": "date-time" },
"empty": { "type": "boolean" }
}
}
}
}
}`,
md: 'Every endpoint, grouped by resource. Click a row to expand its contract - parameters, a curl example, the 200 response, response fields, and error codes. `Live` endpoints are callable today; `Planned` rows are draft contracts from the north-star spec and are not callable yet.',
},
],
},
{
id: 'errors',
title: 'Errors',
blocks: [
{
type: 'p',
md: [
'| Status | Meaning | What to do |',
'| --- | --- | --- |',
'| `401` | Missing, expired, or invalid token. | Re-authenticate and retry with a fresh token. |',
'| `429` | Rate limit exceeded. | Back off and retry after the window resets. |',
'| `503` | Auth service temporarily unavailable (never means a bad token). | Retry with backoff. |',
].join('\n'),
type: 'endpoints',
groups: API_ENDPOINTS,
},
],
},
Expand Down
Loading