Menu

Change language

GitHub repo

Hero Globe Interactive Nodes

blockhero

Global edge latency simulation hero with interactive PoP node inspection and real-time telemetry.

Installation

$npx uipkge-ng@latest add hero-globe-interactive-nodes
Manifest:https://uipkge.dev/r/angular/hero-globe-interactive-nodes.jsonInstalls to:src/app/components/blocks/hero-globe-interactive-nodes/

Variants

Loading interactive previews…

Schema

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

EdgeNode
interface EdgeNode {
  id: string
  name: string
  region: string
  country: string
  city: string
  latencyMs: number
  p99LatencyMs: number
  cacheHitRatio: number
  qps: number
  status: 'optimal' | 'operational' | 'degraded'
  coordinates: { x: number; y: number }
}

npm dependencies

Files installed (2)

  • src/app/components/blocks/hero-globe-interactive-nodes/hero-globe-interactive-nodes.component.ts15.1 kB
    // Global edge latency simulation hero with interactive PoP node inspection and
    // real-time telemetry. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      Activity,
      ArrowRight,
      Check,
      Copy,
      Globe,
      LucideAngularModule,
      Radio,
      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 } from '@/ui/card/card.component'
    
    export interface EdgeNode {
      id: string
      name: string
      region: string
      country: string
      city: string
      latencyMs: number
      p99LatencyMs: number
      cacheHitRatio: number
      qps: number
      status: 'optimal' | 'operational' | 'degraded'
      coordinates: { x: number; y: number }
    }
    
    const DEFAULT_NODES: EdgeNode[] = [
      {
        id: 'node-iad',
        name: 'iad-1',
        region: 'us-east-1',
        country: 'United States',
        city: 'Washington, D.C.',
        latencyMs: 8,
        p99LatencyMs: 14,
        cacheHitRatio: 99.4,
        qps: 18450,
        status: 'optimal',
        coordinates: { x: 28, y: 38 },
      },
      {
        id: 'node-sfo',
        name: 'sfo-1',
        region: 'us-west-1',
        country: 'United States',
        city: 'San Francisco',
        latencyMs: 12,
        p99LatencyMs: 22,
        cacheHitRatio: 99.1,
        qps: 14200,
        status: 'optimal',
        coordinates: { x: 18, y: 40 },
      },
      {
        id: 'node-fra',
        name: 'fra-1',
        region: 'eu-central-1',
        country: 'Germany',
        city: 'Frankfurt',
        latencyMs: 15,
        p99LatencyMs: 27,
        cacheHitRatio: 98.9,
        qps: 21300,
        status: 'optimal',
        coordinates: { x: 52, y: 32 },
      },
      {
        id: 'node-sin',
        name: 'sin-1',
        region: 'ap-southeast-1',
        country: 'Singapore',
        city: 'Singapore',
        latencyMs: 24,
        p99LatencyMs: 41,
        cacheHitRatio: 98.6,
        qps: 16800,
        status: 'optimal',
        coordinates: { x: 76, y: 58 },
      },
      {
        id: 'node-nrt',
        name: 'nrt-1',
        region: 'ap-northeast-1',
        country: 'Japan',
        city: 'Tokyo',
        latencyMs: 19,
        p99LatencyMs: 34,
        cacheHitRatio: 99.2,
        qps: 19400,
        status: 'optimal',
        coordinates: { x: 84, y: 42 },
      },
      {
        id: 'node-syd',
        name: 'syd-1',
        region: 'ap-southeast-2',
        country: 'Australia',
        city: 'Sydney',
        latencyMs: 32,
        p99LatencyMs: 52,
        cacheHitRatio: 98.2,
        qps: 9800,
        status: 'optimal',
        coordinates: { x: 88, y: 78 },
      },
      {
        id: 'node-gru',
        name: 'gru-1',
        region: 'sa-east-1',
        country: 'Brazil',
        city: 'São Paulo',
        latencyMs: 44,
        p99LatencyMs: 68,
        cacheHitRatio: 97.9,
        qps: 7600,
        status: 'optimal',
        coordinates: { x: 36, y: 74 },
      },
      {
        id: 'node-lhr',
        name: 'lhr-1',
        region: 'eu-west-2',
        country: 'United Kingdom',
        city: 'London',
        latencyMs: 11,
        p99LatencyMs: 20,
        cacheHitRatio: 99.5,
        qps: 24600,
        status: 'optimal',
        coordinates: { x: 48, y: 30 },
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-hero-globe-interactive-nodes, [ui-hero-globe-interactive-nodes]',
      standalone: true,
      // React renders the root <section> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent],
      template: `
        <section data-slot="hero-globe-interactive-nodes" [class]="rootClass">
          <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
            <div class="grid grid-cols-1 gap-12 lg:grid-cols-12 lg:items-center">
              <!-- Left: Copy & Actions (6 Cols) -->
              <div class="space-y-6 lg:col-span-6">
                <div
                  class="border-primary/20 bg-primary/10 text-primary inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium shadow-2xs"
                >
                  <lucide-icon [img]="Radio" class="text-primary size-3.5 animate-pulse" />
                  <span>Global Edge CDN Mesh Active &bull; 310+ PoPs</span>
                </div>
    
                <h1 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">
                  {{ title }}
                </h1>
    
                <p class="text-muted-foreground text-base sm:text-lg">
                  {{ description }}
                </p>
    
                <!-- CLI Command Sandbox -->
                <div class="flex flex-col gap-3 sm:flex-row sm:items-center">
                  <div
                    class="border-border bg-muted/60 text-foreground flex items-center justify-between rounded-lg border px-3 py-2 font-mono text-xs shadow-2xs sm:w-80"
                  >
                    <div class="flex items-center gap-2 truncate">
                      <lucide-icon [img]="Terminal" class="text-muted-foreground size-3.5 shrink-0" />
                      <span class="truncate">{{ cliCommand }}</span>
                    </div>
                    <button
                      ui-button
                      variant="ghost"
                      size="icon"
                      type="button"
                      class="text-muted-foreground hover:text-foreground size-7 shrink-0"
                      aria-label="Copy CLI command"
                      (click)="copyCommand()"
                    >
                      @if (copied()) {
                        <lucide-icon [img]="Check" class="text-primary size-3.5" />
                      } @else {
                        <lucide-icon [img]="Copy" class="size-3.5" />
                      }
                    </button>
                  </div>
    
                  <button ui-button size="default" type="button" class="gap-1.5 shadow-xs">
                    <span>Explore Registry</span>
                    <lucide-icon [img]="ArrowRight" class="size-4" />
                  </button>
                </div>
    
                <!-- Quick Telemetry Strip -->
                <div class="border-border grid grid-cols-3 gap-4 border-t pt-6">
                  <div class="space-y-1">
                    <div class="text-muted-foreground text-xs font-medium">Global Avg Ping</div>
                    <div class="text-foreground text-xl font-bold tracking-tight">{{ avgGlobalLatency() }}ms</div>
                    <div class="text-success text-xs font-medium">&minus;42% vs origin</div>
                  </div>
                  <div class="space-y-1">
                    <div class="text-muted-foreground text-xs font-medium">Network Throughput</div>
                    <div class="text-foreground text-xl font-bold tracking-tight">{{ totalGlobalQps() }}</div>
                    <div class="text-muted-foreground text-xs">req/sec live</div>
                  </div>
                  <div class="space-y-1">
                    <div class="text-muted-foreground text-xs font-medium">Edge Availability</div>
                    <div class="text-foreground text-xl font-bold tracking-tight">99.95%</div>
                    <div class="text-success text-xs font-medium">Enterprise SLA</div>
                  </div>
                </div>
              </div>
    
              <!-- Right: Interactive SVG Node Map & Telemetry Workbench (6 Cols) -->
              <div class="space-y-4 lg:col-span-6">
                <div ui-card class="border-border bg-card shadow-sm">
                  <div ui-card-content class="space-y-4 p-4 sm:p-6">
                    <!-- Top bar: active node switch -->
                    <div class="border-border flex items-center justify-between border-b pb-3">
                      <div class="flex items-center gap-2">
                        <lucide-icon [img]="Globe" class="text-primary size-4" />
                        <span class="text-foreground text-xs font-semibold">Active Edge Inspection</span>
                      </div>
                      <span ui-badge variant="outline" class="border-border text-muted-foreground font-mono text-xs">
                        {{ selectedNode().city }} ({{ selectedNode().name }})
                      </span>
                    </div>
    
                    <!-- Interactive Node Grid / Map Canvas -->
                    <div class="border-border/80 bg-muted/30 relative h-60 w-full overflow-hidden rounded-lg border p-3">
                      <!-- SVG Connections Network Layer -->
                      <svg
                        class="stroke-border/70 absolute inset-0 size-full"
                        xmlns="http://www.w3.org/2000/svg"
                        aria-hidden="true"
                      >
                        @for (n of activeNodes().slice(1); track n.id) {
                          <line
                            [attr.x1]="hubNode().coordinates.x + '%'"
                            [attr.y1]="hubNode().coordinates.y + '%'"
                            [attr.x2]="n.coordinates.x + '%'"
                            [attr.y2]="n.coordinates.y + '%'"
                            stroke-dasharray="3 3"
                            stroke-width="1"
                          />
                        }
                      </svg>
    
                      <!-- Interactive Region Nodes -->
                      @for (node of activeNodes(); track node.id) {
                        <button
                          type="button"
                          [style.left]="node.coordinates.x + '%'"
                          [style.top]="node.coordinates.y + '%'"
                          [class]="nodeButtonClass(node.id)"
                          [attr.aria-label]="'Inspect ' + node.name + ' in ' + node.city"
                          (click)="selectNode(node.id)"
                        >
                          <span class="font-mono text-xs leading-none font-bold">
                            {{ selectedNodeId() === node.id ? node.name.slice(0, 3).toUpperCase() : '' }}
                          </span>
                          @if (selectedNodeId() !== node.id) {
                            <span class="bg-success size-2 rounded-full"></span>
                          }
                        </button>
                      }
                    </div>
    
                    <!-- Edge Node Telemetry Card -->
                    <div class="border-border bg-background grid grid-cols-2 gap-2.5 rounded-lg border p-3 sm:grid-cols-4">
                      <div class="space-y-0.5">
                        <div class="text-muted-foreground flex items-center gap-1 text-xs">
                          <lucide-icon [img]="Zap" class="text-primary size-3" />
                          <span>Ping (p50)</span>
                        </div>
                        <div class="text-foreground font-mono text-sm font-bold">{{ selectedNode().latencyMs }}ms</div>
                      </div>
    
                      <div class="space-y-0.5">
                        <div class="text-muted-foreground flex items-center gap-1 text-xs">
                          <lucide-icon [img]="Activity" class="text-success size-3" />
                          <span>Ping (p99)</span>
                        </div>
                        <div class="text-foreground font-mono text-sm font-bold">{{ selectedNode().p99LatencyMs }}ms</div>
                      </div>
    
                      <div class="space-y-0.5">
                        <div class="text-muted-foreground flex items-center gap-1 text-xs">
                          <lucide-icon [img]="Server" class="text-info size-3" />
                          <span>Cache Hit</span>
                        </div>
                        <div class="text-foreground font-mono text-sm font-bold">{{ selectedNode().cacheHitRatio }}%</div>
                      </div>
    
                      <div class="space-y-0.5">
                        <div class="text-muted-foreground flex items-center gap-1 text-xs">
                          <lucide-icon [img]="ShieldCheck" class="text-primary size-3" />
                          <span>Throughput</span>
                        </div>
                        <div class="text-foreground font-mono text-sm font-bold">
                          {{ selectedNode().qps.toLocaleString() }} qps
                        </div>
                      </div>
                    </div>
    
                    <!-- Quick Node Pill Selector -->
                    <div class="flex flex-wrap gap-1.5 pt-1">
                      @for (node of activeNodes(); track node.id) {
                        <button type="button" [class]="nodePillClass(node.id)" (click)="selectNode(node.id)">
                          {{ node.name }} &bull; {{ node.city }}
                        </button>
                      }
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </section>
      `,
    })
    export class UiHeroGlobeInteractiveNodesComponent {
      protected readonly Activity = Activity
      protected readonly ArrowRight = ArrowRight
      protected readonly Check = Check
      protected readonly Copy = Copy
      protected readonly Globe = Globe
      protected readonly Radio = Radio
      protected readonly Server = Server
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Terminal = Terminal
      protected readonly Zap = Zap
    
      @Input() title = 'Sub-millisecond UI edge delivery across 310+ PoPs globally.'
      @Input() description =
        'Compile unbundled Vue 3.5 and React 19 components with instant edge caching. Zero cold starts, deterministic tree-shaking, and hardware-accelerated telemetry.'
      @Input() cliCommand = 'npx shadcn add @uipkge/init'
      @Input() nodes?: EdgeNode[]
      @Input('class') className?: string
    
      readonly selectedNodeId = signal('node-iad')
      readonly copied = signal(false)
    
      private copyTimer: ReturnType<typeof setTimeout> | undefined
    
      readonly activeNodes = computed(() => this.nodes ?? DEFAULT_NODES)
    
      readonly hubNode = computed<EdgeNode>(() => this.activeNodes()[0] ?? DEFAULT_NODES[0]!)
    
      readonly selectedNode = computed<EdgeNode>(() => {
        const all = this.activeNodes()
        return all.find((n) => n.id === this.selectedNodeId()) ?? all[0] ?? DEFAULT_NODES[0]!
      })
    
      readonly avgGlobalLatency = computed(() => {
        const all = this.activeNodes()
        if (!all.length) return 0
        return Math.round(all.reduce((acc, curr) => acc + curr.latencyMs, 0) / all.length)
      })
    
      readonly totalGlobalQps = computed(() =>
        this.activeNodes()
          .reduce((acc, curr) => acc + curr.qps, 0)
          .toLocaleString(),
      )
    
      get rootClass(): string {
        return cn('bg-background relative overflow-hidden py-16 sm:py-24 lg:py-28', this.className)
      }
    
      nodeButtonClass(id: string): string {
        return cn(
          'focus-visible:ring-ring absolute flex -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none',
          this.selectedNodeId() === id
            ? 'bg-primary text-primary-foreground ring-primary/20 z-10 size-7 scale-110 shadow-sm ring-4'
            : 'bg-card hover:bg-accent border-border text-foreground size-5 border hover:scale-105',
        )
      }
    
      nodePillClass(id: string): string {
        return cn(
          'rounded-md border px-2 py-1 font-mono text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none',
          this.selectedNodeId() === id
            ? 'border-primary bg-primary/10 text-primary'
            : 'border-border bg-card text-muted-foreground hover:text-foreground hover:bg-accent/50',
        )
      }
    
      selectNode(id: string): void {
        this.selectedNodeId.set(id)
      }
    
      async copyCommand(): Promise<void> {
        try {
          await navigator.clipboard.writeText(this.cliCommand)
          this.copied.set(true)
          clearTimeout(this.copyTimer)
          this.copyTimer = setTimeout(() => this.copied.set(false), 2000)
        } catch {
          // Clipboard unavailable (e.g. insecure context): leave the Copy icon.
        }
      }
    }
    
  • src/app/components/blocks/hero-globe-interactive-nodes/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/hero-globe-interactive-nodes.json