Menu

Change language

GitHub repo

Api Documentation Page

blockdevops

Stripe 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

$npx uipkge-ng@latest add api-documentation-page
Manifest:https://uipkge.dev/r/angular/api-documentation-page.jsonInstalls to:src/app/components/blocks/api-documentation-page/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

ApiDocHeader
interface ApiDocHeader {
  name: string
  type: string
  required: boolean
  sample: string
  description: string
}
ApiDocNestedParam
interface ApiDocNestedParam {
  name: string
  type: string
  required: boolean
  description: string
  example?: string
}
ApiDocBodyParam
interface ApiDocBodyParam {
  name: string
  type: string
  required: boolean
  description: string
  example?: string
  nested?: ApiDocNestedParam[]
}
ApiDocResponseAttr
interface ApiDocResponseAttr {
  name: string
  type: string
  description: string
  example: string
}
ApiDocStatusPill
interface ApiDocStatusPill {
  code: ApiDocStatus
  label: string
  summary: string
  dotColor: string
}
LanguageOption
interface 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

Raw manifest:https://uipkge.dev/r/angular/api-documentation-page.json