Release Timeline
A dated changelog on a vertical rail with added/changed/fixed tags and machine-readable <time> values.
Method badge, path, parameter table and a request/response pair kept side by side — an example request whose response is a scroll away gets pasted blind.
components/api-endpoint-card.tsxnpx hoverlab add api-endpoint-card
Or over MCP, from your editor's agent — no account needed.
Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.
Start a page with this section — add more, order them, and leave with the page source.
/v1/projects/{project_id}/deploymentsList the deployments for a project, newest first.
| Name | Type | Required | Description |
|---|---|---|---|
| project_id | string | Required | The project to list deployments for. Passed in the path. |
| status | string | Optional | Filter by state: building, ready or failed. |
| limit | integer | Optional | Page size, 1–100. Defaults to 20. |
| cursor | string | Optional | Opaque cursor from a previous page’s next_cursor. |
curl "https://api.acme.dev/v1/projects/prj_8kq2/deployments?limit=2" \
-H "Authorization: Bearer $ACME_API_KEY"{
"data": [
{
"id": "dep_9f2c1a",
"status": "ready",
"branch": "main",
"created_at": "2026-08-12T09:41:00Z"
},
{
"id": "dep_8e1b0d",
"status": "failed",
"branch": "fix/session-ttl",
"created_at": "2026-08-11T17:03:12Z"
}
],
"next_cursor": "dep_7d0a9c"
}Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.
'use client'
/**
* <ApiEndpointCard> — one REST endpoint, documented on a single card.
*
* Request and response sit side by side because an example request whose
* response is a scroll away gets pasted blind: the reader copies the curl,
* runs it, and only then discovers what shape came back. Keeping the pair
* in one glance is the whole reference — the prose is commentary.
*
* The parameters live in a real <table>, not a definition list styled to
* look like one, because required-versus-optional is a column you scan
* down, and screen readers get the same scan for free.
*/
import * as React from 'react'
import { Copy, Check } from 'lucide-react'
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
export interface EndpointParameter {
name: string
type: string
required: boolean
description: string
}
export interface ApiEndpointCardProps {
method?: HttpMethod
path?: string
description?: string
parameters?: EndpointParameter[]
requestExample?: string
responseExample?: string
responseStatus?: string
className?: string
}
const METHOD_STYLES: Record<HttpMethod, string> = {
GET: 'bg-emerald-500/15 text-emerald-500',
POST: 'bg-sky-500/15 text-sky-500',
PUT: 'bg-amber-500/15 text-amber-500',
PATCH: 'bg-amber-500/15 text-amber-500',
DELETE: 'bg-rose-500/15 text-rose-500',
}
const DEFAULT_PARAMETERS: EndpointParameter[] = [
{
name: 'project_id',
type: 'string',
required: true,
description: 'The project to list deployments for. Passed in the path.',
},
{
name: 'status',
type: 'string',
required: false,
description: 'Filter by state: building, ready or failed.',
},
{
name: 'limit',
type: 'integer',
required: false,
description: 'Page size, 1–100. Defaults to 20.',
},
{
name: 'cursor',
type: 'string',
required: false,
description: 'Opaque cursor from a previous page’s next_cursor.',
},
]
const DEFAULT_REQUEST = `curl "https://api.acme.dev/v1/projects/prj_8kq2/deployments?limit=2" \\
-H "Authorization: Bearer $ACME_API_KEY"`
const DEFAULT_RESPONSE = `{
"data": [
{
"id": "dep_9f2c1a",
"status": "ready",
"branch": "main",
"created_at": "2026-08-12T09:41:00Z"
},
{
"id": "dep_8e1b0d",
"status": "failed",
"branch": "fix/session-ttl",
"created_at": "2026-08-11T17:03:12Z"
}
],
"next_cursor": "dep_7d0a9c"
}`
/** Split "/v1/projects/{project_id}/…" so the placeholders read as placeholders. */
function renderPath(path: string) {
return path.split(/(\{[^}]+\})/).map((segment, i) =>
segment.startsWith('{') ? (
<span key={i} className="text-amber-500">
{segment}
</span>
) : (
<span key={i}>{segment}</span>
),
)
}
export function ApiEndpointCard({
method = 'GET',
path = '/v1/projects/{project_id}/deployments',
description = 'List the deployments for a project, newest first.',
parameters = DEFAULT_PARAMETERS,
requestExample = DEFAULT_REQUEST,
responseExample = DEFAULT_RESPONSE,
responseStatus = '200 OK',
className = '',
}: ApiEndpointCardProps) {
const [copied, setCopied] = React.useState(false)
function copyPath() {
if (typeof navigator === 'undefined' || !navigator.clipboard) return
/*
* The `.catch` is not defensive padding.
*
* `writeText` rejects on a denied permission, an insecure origin, or an
* unfocused document, and a promise chain with only a `.then` turns each
* of those into an unhandled rejection in the console of whoever
* installed this block. Nothing visible happens either way — this is a
* copy button — so the failure is swallowed on purpose rather than
* surfaced, but it is swallowed deliberately rather than escaping.
*/
void navigator.clipboard
.writeText(path)
.then(() => {
setCopied(true)
window.setTimeout(() => setCopied(false), 1600)
})
.catch(() => {})
}
return (
<section
className={`w-full rounded-2xl border border-border/60 bg-card/60 p-6 ${className}`}
>
<div className="flex flex-wrap items-center gap-3">
<span
className={`rounded-md px-2 py-1 font-mono text-xs font-bold ${METHOD_STYLES[method]}`}
>
{method}
</span>
<code className="min-w-0 truncate font-mono text-sm text-foreground">
{renderPath(path)}
</code>
<button
type="button"
onClick={copyPath}
aria-label="Copy endpoint path"
className="inline-flex items-center rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
{copied ? (
<Check aria-hidden className="h-3.5 w-3.5 text-emerald-500" />
) : (
<Copy aria-hidden className="h-3.5 w-3.5" />
)}
</button>
</div>
<p className="mt-2 text-sm text-muted-foreground">{description}</p>
<h3 data-stress-ignore className="mt-6 text-sm font-semibold tracking-tight">
Parameters
</h3>
<div className="mt-2 overflow-x-auto rounded-xl border border-border/60">
<table className="w-full text-start text-sm">
<thead>
<tr className="border-b border-border/60 bg-muted/50">
<th scope="col" className="px-4 py-2.5 font-medium">
Name
</th>
<th scope="col" className="px-4 py-2.5 font-medium">
Type
</th>
<th scope="col" className="px-4 py-2.5 font-medium">
Required
</th>
<th scope="col" className="px-4 py-2.5 font-medium">
Description
</th>
</tr>
</thead>
<tbody>
{parameters.map((param) => (
<tr key={param.name} className="border-b border-border/60 last:border-b-0">
<td className="px-4 py-2.5 font-mono text-xs">{param.name}</td>
<td className="px-4 py-2.5 font-mono text-xs text-muted-foreground">
{param.type}
</td>
<td className="px-4 py-2.5">
{param.required ? (
<span className="text-xs font-medium text-amber-500">Required</span>
) : (
<span className="text-xs text-muted-foreground">Optional</span>
)}
</td>
<td className="px-4 py-2.5 text-muted-foreground">{param.description}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-6 grid gap-4 lg:grid-cols-2">
<div className="min-w-0 overflow-hidden rounded-xl border border-border/60 bg-zinc-950">
<div className="flex items-center border-b border-white/10 px-4 py-2.5">
<span className="font-mono text-xs text-white/60">Request</span>
</div>
<pre className="overflow-x-auto p-4 text-xs leading-relaxed">
<code className="font-mono text-zinc-300">{requestExample}</code>
</pre>
</div>
<div className="min-w-0 overflow-hidden rounded-xl border border-border/60 bg-zinc-950">
<div className="flex items-center justify-between border-b border-white/10 px-4 py-2.5">
<span className="font-mono text-xs text-white/60">Response</span>
<span className="rounded-md bg-emerald-500/15 px-1.5 py-0.5 font-mono text-xs font-medium text-emerald-400">
{responseStatus}
</span>
</div>
<pre className="overflow-x-auto p-4 text-xs leading-relaxed">
<code className="font-mono text-zinc-300">{responseExample}</code>
</pre>
</div>
</div>
</section>
)
}
bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.Drop it at components/api-endpoint-card.tsx and import it where you need the section:
import { ApiEndpointCard } from '@/components/api-endpoint-card'3 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.
Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.
| Prop | Type | Default |
|---|---|---|
method | HttpMethod | 'GET' |
path | string | '/v1/projects/{project_id}/deployments' |
description | string | 'List the deployments for a project, newest f… |
parameters | EndpointParameter[] | DEFAULT_PARAMETERS |
requestExample | string | DEFAULT_REQUEST |
responseExample | string | DEFAULT_RESPONSE |
responseStatus | string | '200 OK' |
className | string | '' |
The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.
This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.
<!--
API Endpoint Reference — markup from the Hoverlab catalog.
This is the block rendered once to HTML and wrapped as a component
file. It is not a port of the React source: the Tailwind classes carry
the design, which is the part that took the work, and they are the same
in every framework.
This block is interactive in React and the handlers are NOT here.
Buttons, toggles and menus render in their initial state and do
nothing until you wire them up.
-->
<section class="w-full rounded-2xl border border-border/60 bg-card/60 p-6 ">
<div class="flex flex-wrap items-center gap-3">
<span class="rounded-md px-2 py-1 font-mono text-xs font-bold bg-emerald-500/15 text-emerald-500">GET</span>
<code class="min-w-0 truncate font-mono text-sm text-foreground">
<span>/v1/projects/</span>
<span class="text-amber-500">{project_id}</span>
<span>/deployments</span>
</code>
<button type="button" aria-label="Copy endpoint path" class="inline-flex items-center rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy h-3.5 w-3.5" aria-hidden="true">
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect>
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path>
</svg>
</button>
</div>
<p class="mt-2 text-sm text-muted-foreground">List the deployments for a project, newest first.</p>
<h3 data-stress-ignore="true" class="mt-6 text-sm font-semibold tracking-tight">Parameters</h3>
<div class="mt-2 overflow-x-auto rounded-xl border border-border/60">
<table class="w-full text-start text-sm">
<thead>
<tr class="border-b border-border/60 bg-muted/50">
<th scope="col" class="px-4 py-2.5 font-medium">Name</th>
<th scope="col" class="px-4 py-2.5 font-medium">Type</th>
<th scope="col" class="px-4 py-2.5 font-medium">Required</th>
<th scope="col" class="px-4 py-2.5 font-medium">Description</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-border/60 last:border-b-0">
<td class="px-4 py-2.5 font-mono text-xs">project_id</td>
<td class="px-4 py-2.5 font-mono text-xs text-muted-foreground">string</td>
<td class="px-4 py-2.5">
<span class="text-xs font-medium text-amber-500">Required</span>
</td>
<td class="px-4 py-2.5 text-muted-foreground">The project to list deployments for. Passed in the path.</td>
</tr>
<tr class="border-b border-border/60 last:border-b-0">
<td class="px-4 py-2.5 font-mono text-xs">status</td>
<td class="px-4 py-2.5 font-mono text-xs text-muted-foreground">string</td>
<td class="px-4 py-2.5">
<span class="text-xs text-muted-foreground">Optional</span>
</td>
<td class="px-4 py-2.5 text-muted-foreground">Filter by state: building, ready or failed.</td>
</tr>
<tr class="border-b border-border/60 last:border-b-0">
<td class="px-4 py-2.5 font-mono text-xs">limit</td>
<td class="px-4 py-2.5 font-mono text-xs text-muted-foreground">integer</td>
<td class="px-4 py-2.5">
<span class="text-xs text-muted-foreground">Optional</span>
</td>
<td class="px-4 py-2.5 text-muted-foreground">Page size, 1–100. Defaults to 20.</td>
</tr>
<tr class="border-b border-border/60 last:border-b-0">
<td class="px-4 py-2.5 font-mono text-xs">cursor</td>
<td class="px-4 py-2.5 font-mono text-xs text-muted-foreground">string</td>
<td class="px-4 py-2.5">
<span class="text-xs text-muted-foreground">Optional</span>
</td>
<td class="px-4 py-2.5 text-muted-foreground">Opaque cursor from a previous page’s next_cursor.</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-6 grid gap-4 lg:grid-cols-2">
<div class="min-w-0 overflow-hidden rounded-xl border border-border/60 bg-zinc-950">
<div class="flex items-center border-b border-white/10 px-4 py-2.5">
<span class="font-mono text-xs text-white/60">Request</span>
</div>
<pre class="overflow-x-auto p-4 text-xs leading-relaxed"><code class="font-mono text-zinc-300">curl "https://api.acme.dev/v1/projects/prj_8kq2/deployments?limit=2" \
-H "Authorization: Bearer $ACME_API_KEY"</code></pre>
</div>
<div class="min-w-0 overflow-hidden rounded-xl border border-border/60 bg-zinc-950">
<div class="flex items-center justify-between border-b border-white/10 px-4 py-2.5">
<span class="font-mono text-xs text-white/60">Response</span>
<span class="rounded-md bg-emerald-500/15 px-1.5 py-0.5 font-mono text-xs font-medium text-emerald-400">200 OK</span>
</div>
<pre class="overflow-x-auto p-4 text-xs leading-relaxed"><code class="font-mono text-zinc-300">{
"data": [
{
"id": "dep_9f2c1a",
"status": "ready",
"branch": "main",
"created_at": "2026-08-12T09:41:00Z"
},
{
"id": "dep_8e1b0d",
"status": "failed",
"branch": "fix/session-ttl",
"created_at": "2026-08-11T17:03:12Z"
}
],
"next_cursor": "dep_7d0a9c"
}</code></pre>
</div>
</div>
</section>
What each framework gets across the whole catalog — effects convert properly; this rung is markup.
The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.
Want the whole screen instead of this one section? Open a page and copy it entire.
All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.
Browse Content & BlogCopying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.
A dated changelog on a vertical rail with added/changed/fixed tags and machine-readable <time> values.
Shipped / in progress / planned in three columns, with status carried by icon as well as colour.
A featured post at full width above a card grid, with drawn covers built from tokens — a blog index with no image pipeline and no broken-thumbnail state.
A grid of case-study cards that lead with the metric rather than the company name, each one linking out to the full write-up — the block a customer-stories section or a home page needed and only had a full case-study page for.
Kicker, headline, standfirst, byline and share row, with the opening prose and a pull quote included — the type scale of a post, decided once instead of per-article.
An about-page team section on initials avatars rather than photos, so shipping it never waits on headshots and there is no broken-image state to design.
Openings grouped by department with location, type and salary on every row — candidates self-select by craft first, and a flat list makes them scan for it.
Sidebar, article and on-this-page rail — each column answers a different question, and they collapse on mobile in the order that keeps the article alive.
One install command behind npm/pnpm/yarn/bun tabs with a working copy button — every reader uses exactly one package manager, so showing all four wastes three lines.
A CV as a document rather than a timeline of cards — one column, dates on the end edge, and a print layer that puts it on paper without a separate PDF to keep in step.
The watch screen with no playback in it: a ratio-locked stage, a control bar drawn down to the buffered layer, and an up-next rail. Drop a video element in and wire your own player.