Api Documentation Page
blockdevopsStripe and Mintlify style two-pane API documentation reference page with HTTP method badge, endpoint path, headers, request parameter tables with nested schema hints, response attributes, interactive multi-language code snippets (cURL, Node.js, Python, Go, Ruby), and status response tabs with copy actions.
Installation
$pnpm dlx uipkge-ng@latest add api-documentation-page$npx uipkge-ng@latest add api-documentation-page$yarn dlx uipkge-ng@latest add api-documentation-page$bunx uipkge-ng@latest add api-documentation-pageManifest:
https://uipkge.dev/r/angular/api-documentation-page.jsonInstalls to:src/app/components/blocks/api-documentation-page/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
ApiDocHeaderinterface ApiDocHeader {
name: string
type: string
required: boolean
sample: string
description: string
}ApiDocNestedParaminterface ApiDocNestedParam {
name: string
type: string
required: boolean
description: string
example?: string
}ApiDocBodyParaminterface ApiDocBodyParam {
name: string
type: string
required: boolean
description: string
example?: string
nested?: ApiDocNestedParam[]
}ApiDocResponseAttrinterface ApiDocResponseAttr {
name: string
type: string
description: string
example: string
}ApiDocStatusPillinterface ApiDocStatusPill {
code: ApiDocStatus
label: string
summary: string
dotColor: string
}LanguageOptioninterface LanguageOption {
value: ApiDocLanguage
label: string
}Files installed (2)
src/app/components/blocks/api-documentation-page/api-documentation-page.component.ts34.8 kB
// Stripe and Mintlify style two-pane API documentation reference page: HTTP method badge, // endpoint path, request header/body schema tables, response attributes, interactive // multi-language request snippets (cURL, Node.js, Python, Go, Ruby) and status response tabs // with copy actions. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, DestroyRef, Input, inject, signal } from '@angular/core' import { Check, Copy, CornerDownRight, Globe, LucideAngularModule, Server, ShieldCheck, Terminal, Zap, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export type ApiDocLanguage = 'curl' | 'node' | 'python' | 'go' | 'ruby' export type ApiDocStatus = '200' | '400' | '401' export interface ApiDocHeader { name: string type: string required: boolean sample: string description: string } export interface ApiDocNestedParam { name: string type: string required: boolean description: string example?: string } export interface ApiDocBodyParam { name: string type: string required: boolean description: string example?: string nested?: ApiDocNestedParam[] } export interface ApiDocResponseAttr { name: string type: string description: string example: string } export interface ApiDocStatusPill { code: ApiDocStatus label: string summary: string dotColor: string } const headersList: ApiDocHeader[] = [ { name: 'Authorization', type: 'string', required: true, sample: 'Bearer uipkge_live_51Msz...', description: 'Secret API key prefixed with uipkge_live_ or sk_test_. Pass in the HTTP Authorization header.', }, { name: 'Content-Type', type: 'string', required: true, sample: 'application/json', description: 'Specifies the media type of the request body. Must be application/json.', }, { name: 'Idempotency-Key', type: 'string (UUID)', required: false, sample: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d', description: 'Unique client-generated key that prevents duplicate executions if a request is retried.', }, ] const bodyParams: ApiDocBodyParam[] = [ { name: 'customer_id', type: 'string', required: true, description: 'Unique customer identifier for the subscriber. Must begin with the prefix cus_.', example: '"cus_N63vKZbI8O8yWp"', }, { name: 'plan_id', type: 'string', required: true, description: 'Identifier of the subscription tier to enroll the customer into.', example: '"plan_pro_monthly"', }, { name: 'items', type: 'array of objects', required: true, description: 'List of subscription line items defining the base plan and any optional recurring addons.', nested: [ { name: 'items[].price_id', type: 'string', required: true, description: 'Unique price object identifier corresponding to a catalog product.', example: '"price_1Msz82eZvKYlo2C"', }, { name: 'items[].quantity', type: 'integer', required: false, description: 'Unit quantity of the specified price item. Defaults to 1 if omitted.', example: '1', }, ], }, { name: 'payment_method_id', type: 'string', required: false, description: 'Attached payment method ID (pm_...) to charge. If omitted, uses the customer default payment source.', example: '"pm_1OdVq82eZvKYlo2C"', }, { name: 'billing_cycle_anchor', type: 'integer (timestamp)', required: false, description: 'Future Unix timestamp that marks the beginning of recurring billing cycles.', example: '1717200000', }, { name: 'trial_period_days', type: 'integer', required: false, description: 'Number of zero-charge trial days before regular billing begins (1 to 90).', example: '14', }, { name: 'coupon_code', type: 'string', required: false, description: 'Valid discount or promotional coupon applied to initial invoices.', example: '"LAUNCH2026"', }, { name: 'metadata', type: 'object', required: false, description: 'Arbitrary key-value map for attribution and custom IDs. Supports up to 50 keys, max 500 chars/value.', example: '{"referrer": "onboarding_flow"}', }, ] const responseAttributes: ApiDocResponseAttr[] = [ { name: 'id', type: 'string', description: 'Unique identifier for the subscription entity, prefixed with sub_.', example: '"sub_1Om48B2eZvKYlo2CqO8k2"', }, { name: 'object', type: 'string', description: 'String representing the object type. Always subscription.', example: '"subscription"', }, { name: 'customer', type: 'string', description: 'Customer identifier associated with this subscription record.', example: '"cus_N63vKZbI8O8yWp"', }, { name: 'status', type: 'string', description: 'Current lifecycle status: trialing, active, past_due, canceled, unpaid, or incomplete.', example: '"trialing"', }, { name: 'current_period_start', type: 'integer (timestamp)', description: 'Unix timestamp marking the start of the current active billing cycle.', example: '1717200000', }, { name: 'current_period_end', type: 'integer (timestamp)', description: 'Unix timestamp marking the end of the current cycle and scheduled renewal date.', example: '1719792000', }, { name: 'trial_end', type: 'integer (timestamp) | null', description: 'Unix timestamp when the free trial period concludes and billing begins.', example: '1718409600', }, { name: 'cancel_at_period_end', type: 'boolean', description: 'If true, the subscription will terminate automatically at the end of the current period.', example: 'false', }, { name: 'latest_invoice', type: 'string', description: 'Identifier of the most recent invoice generated for this subscription (in_...).', example: '"in_1Om48B2eZvKYlo2Cj891"', }, ] const statusPills: ApiDocStatusPill[] = [ { code: '200', label: '200 OK', summary: 'Subscription created successfully. Returns full subscription entity.', dotColor: 'bg-success', }, { code: '400', label: '400 Bad Request', summary: 'Missing required parameters or malformed JSON payload.', dotColor: 'bg-warning', }, { code: '401', label: '401 Unauthorized', summary: 'Missing, expired, or invalid Bearer secret API key.', dotColor: 'bg-destructive', }, ] const requestSnippets: Record<ApiDocLanguage, string> = { curl: `curl -X POST https://api.acme.com/v1/customers/subscriptions \\ -H "Authorization: Bearer uipkge_live_51Msz..." \\ -H "Content-Type: application/json" \\ -H "Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d" \\ -d '{ "customer_id": "cus_N63vKZbI8O8yWp", "plan_id": "plan_pro_monthly", "payment_method_id": "pm_1OdVq82eZvKYlo2C", "billing_cycle_anchor": 1717200000, "items": [ { "price_id": "price_1Msz82eZvKYlo2C", "quantity": 1 }, { "price_id": "price_addon_seats_pro", "quantity": 5 } ], "trial_period_days": 14, "coupon_code": "LAUNCH2026", "metadata": { "referrer": "onboarding_flow", "account_manager": "priya.raman" } }'`, node: `import Acme from '@acme/sdk' const acme = new Acme({ apiKey: process.env.ACME_SECRET_KEY, }) const subscription = await acme.subscriptions.create({ customerId: 'cus_N63vKZbI8O8yWp', planId: 'plan_pro_monthly', paymentMethodId: 'pm_1OdVq82eZvKYlo2C', billingCycleAnchor: 1717200000, items: [ { priceId: 'price_1Msz82eZvKYlo2C', quantity: 1 }, { priceId: 'price_addon_seats_pro', quantity: 5 }, ], trialPeriodDays: 14, couponCode: 'LAUNCH2026', metadata: { referrer: 'onboarding_flow', accountManager: 'priya.raman', }, }, { idempotencyKey: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d', }) console.log(subscription.id)`, python: `import os import acme acme.api_key = os.environ.get("ACME_SECRET_KEY") subscription = acme.Subscription.create( customer_id="cus_N63vKZbI8O8yWp", plan_id="plan_pro_monthly", payment_method_id="pm_1OdVq82eZvKYlo2C", billing_cycle_anchor=1717200000, items=[ {"price_id": "price_1Msz82eZvKYlo2C", "quantity": 1}, {"price_id": "price_addon_seats_pro", "quantity": 5}, ], trial_period_days=14, coupon_code="LAUNCH2026", metadata={ "referrer": "onboarding_flow", "account_manager": "priya.raman", }, idempotency_key="9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", ) print(subscription.id)`, go: `package main import ( "context" "fmt" "os" "github.com/acme/acme-go" ) func main() { client := acme.NewClient(os.Getenv("ACME_SECRET_KEY")) params := &acme.SubscriptionCreateParams{ CustomerID: acme.String("cus_N63vKZbI8O8yWp"), PlanID: acme.String("plan_pro_monthly"), PaymentMethodID: acme.String("pm_1OdVq82eZvKYlo2C"), Items: []*acme.SubscriptionItemParams{ {PriceID: acme.String("price_1Msz82eZvKYlo2C"), Quantity: acme.Int64(1)}, {PriceID: acme.String("price_addon_seats_pro"), Quantity: acme.Int64(5)}, }, TrialPeriodDays: acme.Int64(14), CouponCode: acme.String("LAUNCH2026"), Metadata: map[string]string{ "referrer": "onboarding_flow", "account_manager": "priya.raman", }, } sub, err := client.Subscriptions.New(context.Background(), params) if err != nil { panic(err) } fmt.Printf("Created subscription: %s\\n", sub.ID) }`, ruby: `require "acme" Acme.api_key = ENV["ACME_SECRET_KEY"] subscription = Acme::Subscription.create( { customer_id: "cus_N63vKZbI8O8yWp", plan_id: "plan_pro_monthly", payment_method_id: "pm_1OdVq82eZvKYlo2C", billing_cycle_anchor: 1717200000, items: [ { price_id: "price_1Msz82eZvKYlo2C", quantity: 1 }, { price_id: "price_addon_seats_pro", quantity: 5 } ], trial_period_days: 14, coupon_code: "LAUNCH2026", metadata: { referrer: "onboarding_flow", account_manager: "priya.raman" } }, { idempotency_key: "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d" } ) puts subscription.id`, } const responseSnippets: Record<ApiDocStatus, string> = { '200': `{ "id": "sub_1Om48B2eZvKYlo2CqO8k2", "object": "subscription", "customer": "cus_N63vKZbI8O8yWp", "status": "trialing", "plan": { "id": "plan_pro_monthly", "name": "Pro Tier Monthly", "amount": 4900, "currency": "usd", "interval": "month" }, "items": { "object": "list", "data": [ { "id": "si_98f12a3bc4d5", "price": "price_1Msz82eZvKYlo2C", "quantity": 1 }, { "id": "si_98f12a3bc4d6", "price": "price_addon_seats_pro", "quantity": 5 } ], "total_count": 2 }, "current_period_start": 1717200000, "current_period_end": 1719792000, "trial_start": 1717200000, "trial_end": 1718409600, "cancel_at_period_end": false, "latest_invoice": "in_1Om48B2eZvKYlo2Cj891", "metadata": { "account_manager": "priya.raman", "referrer": "onboarding_flow" }, "created_at": 1717200000 }`, '400': `{ "error": { "type": "invalid_request_error", "code": "parameter_missing", "param": "customer_id", "message": "Missing required param: customer_id. Please provide a valid customer identifier.", "doc_url": "https://api.acme.com/docs/errors#parameter_missing" } }`, '401': `{ "error": { "type": "authentication_error", "code": "invalid_api_key", "message": "Invalid API Key provided: uipkge_live_51Msz... Check your secret key in dashboard.", "doc_url": "https://api.acme.com/docs/errors#authentication" } }`, } const ENDPOINT_URL = 'https://api.acme.com/v1/customers/subscriptions' const ENDPOINT_PATH = '/v1/customers/subscriptions' interface LanguageOption { value: ApiDocLanguage label: string } const languageOptions: LanguageOption[] = [ { value: 'curl', label: 'cURL' }, { value: 'node', label: 'Node.js' }, { value: 'python', label: 'Python' }, { value: 'go', label: 'Go' }, { value: 'ruby', label: 'Ruby' }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-api-documentation-page, [ui-api-documentation-page]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiSeparatorComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <div data-slot="api-documentation-page" [class]="rootClass"> <!-- Top Reference Navigation Bar --> <header class="border-border bg-card/60 flex flex-wrap items-center justify-between gap-4 border-b px-4 py-3 sm:px-6" > <div class="flex flex-wrap items-center gap-3"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="Server" class="size-4" /> </div> <div class="flex items-center gap-2"> <span class="text-foreground text-xs font-semibold">Acme Billing API</span> <span class="text-muted-foreground text-xs">/</span> <span class="text-muted-foreground text-xs">Subscriptions</span> <span ui-badge variant="secondary" class="font-mono text-xs font-normal">v2026-03-01</span> </div> </div> <div class="flex flex-wrap items-center gap-2"> <div class="border-border bg-background/80 flex items-center gap-2 rounded-md border px-2.5 py-1 text-xs"> <span class="bg-success size-2 rounded-full"></span> <span class="text-muted-foreground font-mono text-xs">Base:</span> <span class="font-mono font-medium">https://api.acme.com/v1</span> </div> <button ui-button variant="outline" size="sm" class="h-7 gap-1.5 text-xs" (click)="copyEndpoint()"> @if (copiedEndpoint()) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } {{ copiedEndpoint() ? 'Copied URL' : 'Copy Base URL' }} </button> </div> </header> <!-- Two-Column Reference Layout --> <div class="grid grid-cols-1 lg:grid-cols-12"> <!-- Left Main Column: Documentation Prose & Schema Tables --> <main class="border-border space-y-8 p-4 sm:p-6 lg:col-span-7 lg:border-r lg:p-8"> <!-- Endpoint Section Header --> <section class="space-y-4"> <div class="flex flex-wrap items-center gap-2.5"> <span class="border-success/30 bg-success/10 text-success inline-flex items-center rounded-md border px-2.5 py-0.5 font-mono text-xs font-bold tracking-wider" > POST </span> <span class="text-foreground font-mono text-sm font-semibold tracking-tight sm:text-base"> {{ endpointPath }} </span> </div> <div> <h1 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">Create a subscription</h1> <p class="text-muted-foreground mt-2 text-sm leading-relaxed"> Creates a new recurring subscription contract for an existing customer account. If a default payment method is attached, initial recurring invoices are calculated and charged upon trial conclusion or cycle anchor date. </p> </div> <!-- Metadata Badges --> <div class="flex flex-wrap items-center gap-2 pt-1"> <div class="border-border bg-card text-muted-foreground inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs" > <lucide-icon [img]="Zap" class="text-warning size-3.5" /> <span>Idempotent:</span> <span class="text-foreground font-medium">Yes (via Idempotency-Key)</span> </div> <div class="border-border bg-card text-muted-foreground inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs" > <lucide-icon [img]="ShieldCheck" class="text-success size-3.5" /> <span>Auth:</span> <span class="text-foreground font-medium">Bearer Secret Key</span> </div> <div class="border-border bg-card text-muted-foreground inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs" > <lucide-icon [img]="Globe" class="text-foreground/70 size-3.5" /> <span>Rate Limit:</span> <span class="text-foreground font-medium">100 req/min</span> </div> </div> </section> <div ui-separator></div> <!-- Request Headers --> <section class="space-y-3"> <div class="flex items-center justify-between"> <h2 class="text-foreground text-xs font-semibold tracking-wider uppercase">Request Headers</h2> <span ui-badge variant="secondary" class="font-mono text-xs font-normal">HTTP/1.1 & HTTP/2</span> </div> <div ui-card class="border-border overflow-hidden border shadow-none"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header> <tr ui-table-row class="bg-muted/40 hover:bg-muted/40"> <th ui-table-head class="text-xs font-semibold">Header</th> <th ui-table-head class="text-xs font-semibold">Type</th> <th ui-table-head class="text-xs font-semibold">Requirement</th> <th ui-table-head class="text-xs font-semibold">Description</th> </tr> </thead> <tbody ui-table-body> @for (header of headersList; track header.name) { <tr ui-table-row class="text-xs"> <td ui-table-cell class="text-foreground font-mono font-medium">{{ header.name }}</td> <td ui-table-cell> <span ui-badge variant="secondary" class="font-mono text-xs font-normal"> {{ header.type }} </span> </td> <td ui-table-cell> @if (header.required) { <span ui-badge class="border-destructive/20 bg-destructive/10 text-destructive text-xs"> REQUIRED </span> } @else { <span class="text-muted-foreground text-xs">optional</span> } </td> <td ui-table-cell class="text-muted-foreground"> <p>{{ header.description }}</p> <code class="text-foreground/80 mt-1 inline-block font-mono text-xs"> Sample: {{ header.sample }} </code> </td> </tr> } </tbody> </table> </div> </div> </section> <!-- Request Body Parameters --> <section class="space-y-3"> <div class="flex items-center justify-between"> <div class="space-y-0.5"> <h2 class="text-foreground text-xs font-semibold tracking-wider uppercase">Request Body Parameters</h2> <p class="text-muted-foreground text-xs">Provide arguments in a standard JSON payload format.</p> </div> <span ui-badge variant="outline" class="font-mono text-xs font-normal">application/json</span> </div> <div ui-card class="border-border overflow-hidden border shadow-none"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header> <tr ui-table-row class="bg-muted/40 hover:bg-muted/40"> <th ui-table-head class="text-xs font-semibold">Parameter</th> <th ui-table-head class="text-xs font-semibold">Type</th> <th ui-table-head class="text-xs font-semibold">Requirement</th> <th ui-table-head class="text-xs font-semibold">Description & Constraints</th> </tr> </thead> <tbody ui-table-body> @for (param of bodyParams; track param.name) { <tr ui-table-row class="text-xs"> <td ui-table-cell class="text-foreground font-mono font-medium">{{ param.name }}</td> <td ui-table-cell> <span ui-badge variant="secondary" class="font-mono text-xs font-normal"> {{ param.type }} </span> </td> <td ui-table-cell> @if (param.required) { <span ui-badge class="border-destructive/20 bg-destructive/10 text-destructive text-xs"> REQUIRED </span> } @else { <span class="text-muted-foreground text-xs">optional</span> } </td> <td ui-table-cell class="text-muted-foreground"> <p>{{ param.description }}</p> @if (param.example) { <div class="mt-1"> <span class="text-muted-foreground text-xs">Example: </span> <code class="text-foreground font-mono text-xs">{{ param.example }}</code> </div> } </td> </tr> @if (param.nested) { @for (child of param.nested; track child.name) { <tr ui-table-row class="bg-muted/15 text-xs"> <td ui-table-cell class="text-foreground pl-6 font-mono text-xs font-medium"> <div class="flex items-center gap-1.5"> <lucide-icon [img]="CornerDownRight" class="text-muted-foreground size-3 shrink-0" /> <span>{{ child.name }}</span> </div> </td> <td ui-table-cell> <span ui-badge variant="outline" class="font-mono text-xs font-normal"> {{ child.type }} </span> </td> <td ui-table-cell> @if (child.required) { <span ui-badge class="border-destructive/20 bg-destructive/10 text-destructive text-xs"> REQUIRED </span> } @else { <span class="text-muted-foreground text-xs">optional</span> } </td> <td ui-table-cell class="text-muted-foreground"> <p>{{ child.description }}</p> @if (child.example) { <div class="mt-1"> <span class="text-muted-foreground text-xs">Example: </span> <code class="text-foreground font-mono text-xs">{{ child.example }}</code> </div> } </td> </tr> } } } </tbody> </table> </div> </div> </section> <!-- Response Schema Attributes --> <section class="space-y-3"> <div class="space-y-0.5"> <h2 class="text-foreground text-xs font-semibold tracking-wider uppercase">Response Object Attributes</h2> <p class="text-muted-foreground text-xs"> Properties returned in the 200 OK HTTP JSON payload representation. </p> </div> <div ui-card class="border-border overflow-hidden border shadow-none"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header> <tr ui-table-row class="bg-muted/40 hover:bg-muted/40"> <th ui-table-head class="text-xs font-semibold">Attribute</th> <th ui-table-head class="text-xs font-semibold">Type</th> <th ui-table-head class="text-xs font-semibold">Description</th> <th ui-table-head class="text-xs font-semibold">Sample Value</th> </tr> </thead> <tbody ui-table-body> @for (attr of responseAttributes; track attr.name) { <tr ui-table-row class="text-xs"> <td ui-table-cell class="text-foreground font-mono font-medium">{{ attr.name }}</td> <td ui-table-cell> <span ui-badge variant="secondary" class="font-mono text-xs font-normal"> {{ attr.type }} </span> </td> <td ui-table-cell class="text-muted-foreground">{{ attr.description }}</td> <td ui-table-cell class="text-foreground/90 font-mono text-xs">{{ attr.example }}</td> </tr> } </tbody> </table> </div> </div> </section> </main> <!-- Right Column: Interactive Code & Response Inspector --> <aside class="bg-muted/20 space-y-6 p-4 sm:p-6 lg:col-span-5"> <div class="space-y-6 lg:sticky lg:top-6"> <!-- Request Code Card --> <div class="overflow-hidden rounded-xl border border-neutral-800 bg-neutral-950 text-neutral-100 shadow-md"> <div class="flex flex-wrap items-center justify-between border-b border-neutral-800 bg-neutral-900/90 px-3 py-2" > <div class="flex items-center gap-1 overflow-x-auto"> @for (lang of languageOptions; track lang.value) { <button type="button" [class]="langTabClass(lang.value)" (click)="setLanguage(lang.value)"> {{ lang.label }} </button> } </div> <button ui-button variant="ghost" size="sm" class="h-7 shrink-0 gap-1.5 text-xs text-neutral-300 hover:bg-neutral-800 hover:text-neutral-100" (click)="copyRequestSnippet()" > @if (copiedRequest()) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } {{ copiedRequest() ? 'Copied' : 'Copy' }} </button> </div> <div class="flex items-center justify-between border-b border-neutral-800/60 bg-neutral-950/60 px-4 py-1.5" > <div class="flex items-center gap-2"> <lucide-icon [img]="Terminal" class="size-3.5 text-neutral-400" /> <span class="font-mono text-xs text-neutral-400">Request Example</span> </div> <span class="font-mono text-xs text-neutral-500 uppercase">{{ selectedLang() }}</span> </div> <div class="max-h-[360px] overflow-auto p-4 font-mono text-xs leading-relaxed text-neutral-200"> <pre class="whitespace-pre"><code>{{ requestSnippet() }}</code></pre> </div> </div> <!-- Response Code Card --> <div class="overflow-hidden rounded-xl border border-neutral-800 bg-neutral-950 text-neutral-100 shadow-md"> <div class="flex flex-wrap items-center justify-between border-b border-neutral-800 bg-neutral-900/90 px-3 py-2" > <div class="flex items-center gap-1.5 overflow-x-auto"> <span class="mr-1 text-xs font-semibold tracking-wider text-neutral-400 uppercase">Response</span> @for (pill of statusPills; track pill.code) { <button type="button" [class]="statusTabClass(pill.code)" (click)="setStatus(pill.code)"> <span [class]="cn('size-2 rounded-full', pill.dotColor)"></span> <span>{{ pill.label }}</span> </button> } </div> <button ui-button variant="ghost" size="sm" class="h-7 shrink-0 gap-1.5 text-xs text-neutral-300 hover:bg-neutral-800 hover:text-neutral-100" (click)="copyResponseSnippet()" > @if (copiedResponse()) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } {{ copiedResponse() ? 'Copied' : 'Copy' }} </button> </div> <div class="border-b border-neutral-800/60 bg-neutral-950/60 px-4 py-2 text-xs text-neutral-400"> <span>{{ activeStatusSummary() }}</span> </div> <div class="max-h-[380px] overflow-auto p-4 font-mono text-xs leading-relaxed text-neutral-200"> <pre class="whitespace-pre"><code>{{ responseSnippet() }}</code></pre> </div> </div> </div> </aside> </div> </div> `, }) export class UiApiDocumentationPageComponent { protected readonly Check = Check protected readonly Copy = Copy protected readonly CornerDownRight = CornerDownRight protected readonly Globe = Globe protected readonly Server = Server protected readonly ShieldCheck = ShieldCheck protected readonly Terminal = Terminal protected readonly Zap = Zap protected readonly headersList = headersList protected readonly bodyParams = bodyParams protected readonly responseAttributes = responseAttributes protected readonly statusPills = statusPills protected readonly languageOptions = languageOptions protected readonly endpointPath = ENDPOINT_PATH @Input() set initialLanguage(v: ApiDocLanguage) { if (v) this.selectedLang.set(v) } @Input() set initialStatus(v: ApiDocStatus) { if (v) this.selectedStatus.set(v) } @Input('class') className?: string readonly selectedLang = signal<ApiDocLanguage>('curl') readonly selectedStatus = signal<ApiDocStatus>('200') readonly copiedEndpoint = signal(false) readonly copiedRequest = signal(false) readonly copiedResponse = signal(false) private readonly timers: ReturnType<typeof setTimeout>[] = [] constructor() { inject(DestroyRef).onDestroy(() => { for (const t of this.timers) clearTimeout(t) }) } get rootClass(): string { return cn( 'bg-background text-foreground border-border w-full overflow-hidden rounded-xl border shadow-xs', this.className, ) } requestSnippet(): string { return requestSnippets[this.selectedLang()] } responseSnippet(): string { return responseSnippets[this.selectedStatus()] } activeStatusSummary(): string { return statusPills.find((p) => p.code === this.selectedStatus())?.summary ?? '' } langTabClass(lang: ApiDocLanguage): string { return cn( 'rounded-md px-2.5 py-1 font-mono text-xs transition-colors', this.selectedLang() === lang ? 'bg-neutral-800 font-semibold text-neutral-100 shadow-xs' : 'text-neutral-400 hover:bg-neutral-800/50 hover:text-neutral-200', ) } statusTabClass(code: ApiDocStatus): string { return cn( 'flex items-center gap-1.5 rounded-md px-2.5 py-1 font-mono text-xs transition-colors', this.selectedStatus() === code ? 'bg-neutral-800 font-semibold text-neutral-100 shadow-xs' : 'text-neutral-400 hover:bg-neutral-800/50 hover:text-neutral-200', ) } protected cn(...args: Parameters<typeof cn>): string { return cn(...args) } setLanguage(lang: ApiDocLanguage): void { this.selectedLang.set(lang) } setStatus(code: ApiDocStatus): void { this.selectedStatus.set(code) } private flash(target: 'endpoint' | 'request' | 'response'): void { if (target === 'endpoint') this.copiedEndpoint.set(true) if (target === 'request') this.copiedRequest.set(true) if (target === 'response') this.copiedResponse.set(true) this.timers.push( setTimeout(() => { if (target === 'endpoint') this.copiedEndpoint.set(false) if (target === 'request') this.copiedRequest.set(false) if (target === 'response') this.copiedResponse.set(false) }, 2000), ) } async copyEndpoint(): Promise<void> { try { await navigator.clipboard.writeText(ENDPOINT_URL) this.flash('endpoint') } catch { // Clipboard unavailable — skip the feedback swap. } } async copyRequestSnippet(): Promise<void> { try { await navigator.clipboard.writeText(requestSnippets[this.selectedLang()]) this.flash('request') } catch { // Clipboard unavailable — skip the feedback swap. } } async copyResponseSnippet(): Promise<void> { try { await navigator.clipboard.writeText(responseSnippets[this.selectedStatus()]) this.flash('response') } catch { // Clipboard unavailable — skip the feedback swap. } } }src/app/components/blocks/api-documentation-page/index.ts0.2 kB
export { UiApiDocumentationPageComponent, type ApiDocBodyParam, type ApiDocHeader, type ApiDocLanguage, type ApiDocNestedParam, type ApiDocResponseAttr, type ApiDocStatus, type ApiDocStatusPill, } from './api-documentation-page.component'
Raw manifest:https://uipkge.dev/r/angular/api-documentation-page.json