Menu

Change language

GitHub repo

Shipment Tracking Timeline

blocklogistics

Flexport & Maersk style container freight tracking timeline with live AIS vessel telemetry, voyage ETA milestones, Reefer cold-chain specifications, and multimodal transport logistics.

Installation

$npx uipkge-ng@latest add shipment-tracking-timeline
Manifest:https://uipkge.dev/r/angular/shipment-tracking-timeline.jsonInstalls to:src/app/components/blocks/shipment-tracking-timeline/

Variants

Loading interactive previews…

Schema

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

Milestone
interface Milestone {
  id: string
  stepNumber: number
  title: string
  location: string
  facility: string
  country: string
  date: string
  status: MilestoneStatus
  description: string
  coordinates?: string
  telemetry?: {
    speed?: string
    heading?: string
    depth?: string
  }
}

Files installed (2)

  • src/app/components/blocks/shipment-tracking-timeline/shipment-tracking-timeline.component.ts37.3 kB
    // Flexport / Maersk style container freight tracking timeline: header card with vessel
    // and route meta, 4 voyage metric cards, a filterable multimodal journey milestone stepper
    // with live AIS telemetry, plus container specs and shipping documentation. Port of the
    // React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      Anchor,
      ArrowRight,
      Box,
      CalendarClock,
      Check,
      Compass,
      Copy,
      Download,
      FileCheck2,
      FileText,
      LucideAngularModule,
      MapPin,
      Navigation,
      Radio,
      Search,
      Share2,
      ShieldCheck,
      Ship,
      Thermometer,
      Timer,
    } 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 { UiInputComponent } from '@/ui/input/input.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    
    export type MilestoneStatus = 'completed' | 'current' | 'upcoming'
    export type MilestoneFilter = 'all' | 'completed' | 'upcoming'
    
    export interface Milestone {
      id: string
      stepNumber: number
      title: string
      location: string
      facility: string
      country: string
      date: string
      status: MilestoneStatus
      description: string
      coordinates?: string
      telemetry?: {
        speed?: string
        heading?: string
        depth?: string
      }
    }
    
    const defaultMilestones: Milestone[] = [
      {
        id: 'ms-1',
        stepNumber: 1,
        title: 'Cargo Loaded at Factory',
        location: 'Ningbo Depot',
        facility: 'Apex Logistics Hub South',
        country: 'CN',
        date: 'Aug 06, 2026 · 08:30 CST',
        status: 'completed',
        description: 'Container stuffed, Reefer unit pre-cooled to -18°C, bolt seal #SL-94021 applied.',
      },
      {
        id: 'ms-2',
        stepNumber: 2,
        title: 'Export Customs Cleared',
        location: 'Shanghai Port',
        facility: 'Yangshan Customs Gate 4',
        country: 'CN',
        date: 'Aug 08, 2026 · 14:15 CST',
        status: 'completed',
        description: 'Export declaration approved. Customs electronic release note #CN-SH-992 issued.',
      },
      {
        id: 'ms-3',
        stepNumber: 3,
        title: 'Vessel Departed Port',
        location: 'Shanghai Port (CNSHA)',
        facility: 'Yangshan Deepwater Terminal Berth 3',
        country: 'CN',
        date: 'Aug 10, 2026 · 22:00 CST',
        status: 'completed',
        description: 'Loaded onto vessel Ever Given (Voyage V.042W, Bay 42, Tier 04). Ocean transit started.',
      },
      {
        id: 'ms-4',
        stepNumber: 4,
        title: 'Current Position · Sea Transit',
        location: 'Red Sea International Waters',
        facility: 'Maritime Convoy Corridor 2',
        country: 'INT',
        date: 'Aug 21, 2026 · 11:20 UTC (Live)',
        status: 'current',
        description: 'Cruising in transit towards Suez Canal. High-frequency AIS beacon reporting healthy telemetry.',
        coordinates: "24°18'N, 37°42'E",
        telemetry: {
          speed: '18.4 knots',
          heading: '315° NW',
          depth: '840 m',
        },
      },
      {
        id: 'ms-5',
        stepNumber: 5,
        title: 'Suez Canal Transit',
        location: 'Port Said / Suez (EGPSD)',
        facility: 'Suez Canal Authority Convoy Hub',
        country: 'EG',
        date: 'Est. Aug 25, 2026 · 06:00 EEST',
        status: 'upcoming',
        description: 'Scheduled northbound convoy passage slot #04 with authorized maritime pilotage.',
      },
      {
        id: 'ms-6',
        stepNumber: 6,
        title: 'Port of Discharge Arrival',
        location: 'Rotterdam (NLRTM)',
        facility: 'APM Terminals Maasvlakte II',
        country: 'NL',
        date: 'Est. Sep 04, 2026 · 14:00 CEST',
        status: 'upcoming',
        description: 'Vessel berthing & container unlashing operations. Import customs clearance processing.',
      },
      {
        id: 'ms-7',
        stepNumber: 7,
        title: 'Final Destination Delivery',
        location: 'Duisburg Distribution Center',
        facility: 'EuroHub Logistics Terminal',
        country: 'DE',
        date: 'Est. Sep 07, 2026 · 09:00 CEST',
        status: 'upcoming',
        description: 'Intermodal electric rail transfer from Rotterdam to Duisburg bonded warehouse.',
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-shipment-tracking-timeline, [ui-shipment-tracking-timeline]',
      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,
        UiInputComponent,
        UiSeparatorComponent,
      ],
      template: `
        <div data-slot="shipment-tracking-timeline" [class]="rootClass">
          <!-- Header Card -->
          <div ui-card class="border-border bg-card shadow-xs">
            <div ui-card-content class="p-4 sm:p-6">
              <div class="flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between">
                <!-- Left: ID, Vessel & Route -->
                <div class="space-y-3">
                  <div class="flex flex-wrap items-center gap-2 sm:gap-3">
                    <div class="flex items-center gap-1.5">
                      <span class="text-foreground font-mono text-xl font-bold tracking-tight sm:text-2xl">
                        #MSKU-9482014
                      </span>
                      <button
                        ui-button
                        variant="ghost"
                        size="icon-sm"
                        class="text-muted-foreground hover:text-foreground size-7"
                        title="Copy Container ID"
                        (click)="copyContainerId()"
                      >
                        @if (copiedContainerId()) {
                          <lucide-icon [img]="Check" class="text-success size-3.5" aria-hidden="true" />
                        } @else {
                          <lucide-icon [img]="Copy" class="size-3.5" aria-hidden="true" />
                        }
                        <span class="sr-only">Copy container number</span>
                      </button>
                    </div>
    
                    <span
                      ui-badge
                      variant="secondary"
                      class="border-success/30 bg-success/10 text-success gap-1.5 px-2.5 py-0.5 text-xs font-medium"
                    >
                      <span class="relative flex size-2 shrink-0">
                        <span class="bg-success absolute inline-flex h-full w-full rounded-full opacity-75"></span>
                        <span class="bg-success relative inline-flex size-2 rounded-full"></span>
                      </span>
                      In Transit · On Schedule
                    </span>
    
                    <span ui-badge variant="outline" class="border-border bg-muted/30 text-muted-foreground gap-1 text-xs">
                      <lucide-icon [img]="Ship" class="text-primary size-3" aria-hidden="true" />
                      Ocean Freight · FCL 40ft High Cube
                    </span>
                  </div>
    
                  <!-- Route and Vessel meta -->
                  <div class="flex flex-wrap items-center gap-x-4 gap-y-2 text-xs">
                    <div class="text-foreground flex items-center gap-1.5 font-medium">
                      <span class="bg-muted rounded px-1.5 py-0.5 font-mono text-xs">CNSHA</span>
                      <span>Shanghai</span>
                      <lucide-icon [img]="ArrowRight" class="text-muted-foreground size-3.5" aria-hidden="true" />
                      <span class="bg-muted rounded px-1.5 py-0.5 font-mono text-xs">NLRTM</span>
                      <span>Rotterdam</span>
                    </div>
                    <ui-separator orientation="vertical" class="hidden h-3.5 sm:block" />
                    <div class="text-muted-foreground flex items-center gap-1.5">
                      <lucide-icon [img]="Ship" class="text-muted-foreground size-3.5" aria-hidden="true" />
                      <span>Vessel:</span>
                      <span class="text-foreground font-semibold">Ever Given / V.042W</span>
                      <span class="text-muted-foreground font-mono text-xs">(IMO 9811000)</span>
                    </div>
                    <ui-separator orientation="vertical" class="hidden h-3.5 sm:block" />
                    <div class="text-muted-foreground flex items-center gap-1">
                      <span>Carrier:</span>
                      <span class="text-foreground font-medium">Maersk Line</span>
                    </div>
                  </div>
                </div>
    
                <!-- Right: Container search & Quick Actions -->
                <div class="flex flex-col gap-2 sm:flex-row sm:items-center">
                  <div class="relative w-full sm:w-64">
                    <lucide-icon
                      [img]="Search"
                      class="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2"
                      aria-hidden="true"
                    />
                    <ui-input
                      [value]="searchQuery()"
                      (valueChange)="searchQuery.set($event)"
                      placeholder="Track Container / B/L..."
                      class="h-9 pl-8 font-mono text-xs focus-visible:ring-2"
                    />
                  </div>
                  <div class="flex items-center gap-2">
                    <button ui-button size="sm" class="gap-1.5 shadow-xs">
                      <lucide-icon [img]="Search" class="size-3.5" aria-hidden="true" />
                      Track
                    </button>
                    <button ui-button variant="outline" size="sm" class="gap-1.5 shadow-xs" (click)="copyTrackingLink()">
                      <lucide-icon [img]="Share2" class="size-3.5" aria-hidden="true" />
                      <span class="hidden sm:inline">{{ copiedBLLink() ? 'Copied' : 'Share' }}</span>
                    </button>
                  </div>
                </div>
              </div>
            </div>
          </div>
    
          <!-- 4 Summary Metric Cards -->
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            <!-- ETA Card -->
            <div ui-card class="border-border bg-card shadow-xs">
              <div ui-card-content class="p-4 sm:p-5">
                <div class="flex items-start justify-between">
                  <div class="space-y-1">
                    <p class="text-muted-foreground text-xs font-medium tracking-wide uppercase">Estimated Arrival (ETA)</p>
                    <p class="text-foreground font-mono text-base font-bold tabular-nums sm:text-lg">Sep 04, 2026</p>
                    <p class="text-muted-foreground text-xs tabular-nums">14:00 CEST · Berth 4A confirmed</p>
                  </div>
                  <div class="bg-primary/10 text-primary flex size-9 shrink-0 items-center justify-center rounded-lg">
                    <lucide-icon [img]="CalendarClock" class="size-4.5" aria-hidden="true" />
                  </div>
                </div>
                <div
                  class="border-border/60 bg-muted/40 mt-3.5 flex items-center justify-between rounded-md border px-2.5 py-1.5 text-xs"
                >
                  <span class="text-muted-foreground">Remaining</span>
                  <span class="text-success font-medium">14 days left</span>
                </div>
              </div>
            </div>
    
            <!-- Days in Transit Card -->
            <div ui-card class="border-border bg-card shadow-xs">
              <div ui-card-content class="p-4 sm:p-5">
                <div class="flex items-start justify-between">
                  <div class="space-y-1">
                    <p class="text-muted-foreground text-xs font-medium tracking-wide uppercase">Days in Transit</p>
                    <p class="text-foreground font-mono text-base font-bold tabular-nums sm:text-lg">
                      18 <span class="text-muted-foreground text-sm font-normal">of 28 days</span>
                    </p>
                    <p class="text-muted-foreground text-xs tabular-nums">64% of voyage completed</p>
                  </div>
                  <div class="bg-success/10 text-success flex size-9 shrink-0 items-center justify-center rounded-lg">
                    <lucide-icon [img]="Timer" class="size-4.5" aria-hidden="true" />
                  </div>
                </div>
                <div class="mt-3.5 space-y-1.5">
                  <div class="bg-muted h-1.5 w-full overflow-hidden rounded-full">
                    <div class="bg-success h-full rounded-full transition-[width]" style="width: 64%"></div>
                  </div>
                  <div class="text-muted-foreground flex items-center justify-between text-xs">
                    <span>Departed: Aug 10</span>
                    <span>ETA: Sep 04</span>
                  </div>
                </div>
              </div>
            </div>
    
            <!-- Current Coordinates Card -->
            <div ui-card class="border-border bg-card shadow-xs">
              <div ui-card-content class="p-4 sm:p-5">
                <div class="flex items-start justify-between">
                  <div class="space-y-1">
                    <p class="text-muted-foreground text-xs font-medium tracking-wide uppercase">Current Vessel Position</p>
                    <p class="text-foreground font-mono text-base font-bold tabular-nums sm:text-lg">24°18'N, 37°42'E</p>
                    <p class="text-muted-foreground text-xs">
                      Red Sea · Speed <span class="text-foreground font-mono font-medium">18.4 kn</span>
                    </p>
                  </div>
                  <div class="bg-info/10 text-info flex size-9 shrink-0 items-center justify-center rounded-lg">
                    <lucide-icon [img]="Compass" class="size-4.5" aria-hidden="true" />
                  </div>
                </div>
                <div
                  class="border-border/60 bg-muted/40 mt-3.5 flex items-center justify-between rounded-md border px-2.5 py-1.5 text-xs"
                >
                  <span class="text-muted-foreground">Heading</span>
                  <span class="text-foreground font-mono font-medium">315° NW (Convoy)</span>
                </div>
              </div>
            </div>
    
            <!-- Next Milestone Port Card -->
            <div ui-card class="border-border bg-card shadow-xs">
              <div ui-card-content class="p-4 sm:p-5">
                <div class="flex items-start justify-between">
                  <div class="space-y-1">
                    <p class="text-muted-foreground text-xs font-medium tracking-wide uppercase">Next Milestone Port</p>
                    <p class="text-foreground font-mono text-base font-bold sm:text-lg">Suez Canal</p>
                    <p class="text-muted-foreground text-xs tabular-nums">Aug 25 · Northbound Convoy</p>
                  </div>
                  <div class="bg-warning/10 text-warning flex size-9 shrink-0 items-center justify-center rounded-lg">
                    <lucide-icon [img]="Anchor" class="size-4.5" aria-hidden="true" />
                  </div>
                </div>
                <div
                  class="border-border/60 bg-muted/40 mt-3.5 flex items-center justify-between rounded-md border px-2.5 py-1.5 text-xs"
                >
                  <span class="text-muted-foreground">Pilotage Slot</span>
                  <span class="text-foreground font-mono font-medium">Slot #04 (06:00 EEST)</span>
                </div>
              </div>
            </div>
          </div>
    
          <!-- Multimodal Journey Milestones Stepper Card -->
          <div ui-card class="border-border bg-card shadow-xs">
            <div ui-card-header class="flex flex-col gap-3 pb-4 sm:flex-row sm:items-center sm:justify-between">
              <div class="space-y-1">
                <div class="flex items-center gap-2">
                  <lucide-icon [img]="Navigation" class="text-primary size-4.5" aria-hidden="true" />
                  <h3 ui-card-title class="text-base font-semibold">Multimodal Journey Milestones</h3>
                  <span ui-badge variant="outline" class="font-mono text-xs">7 Milestones</span>
                </div>
                <p ui-card-description class="text-xs">
                  End-to-end container tracking from factory loading in Ningbo to warehouse in Duisburg.
                </p>
              </div>
    
              <!-- Filter Buttons -->
              <div class="border-border bg-muted/40 flex items-center gap-1 rounded-lg border p-0.5 text-xs">
                <button type="button" [class]="filterButtonClass('all')"
                  (click)="activeFilter.set('all')"
                >
                  All (7)
                </button>
                <button type="button" [class]="filterButtonClass('completed')"
                  (click)="activeFilter.set('completed')"
                >
                  Completed (3)
                </button>
                <button type="button" [class]="filterButtonClass('upcoming')"
                  (click)="activeFilter.set('upcoming')"
                >
                  Upcoming (3)
                </button>
              </div>
            </div>
    
            <div ui-card-content class="space-y-6 pt-2">
              <!-- Live AIS Telemetry Beacon Banner -->
              <div class="border-primary/30 bg-primary/5 relative overflow-hidden rounded-xl border p-4">
                <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
                  <div class="flex items-center gap-3">
                    <div class="bg-primary/10 text-primary relative flex size-10 items-center justify-center rounded-lg">
                      <lucide-icon [img]="Radio" class="size-5" aria-hidden="true" />
                      <span class="absolute -top-1 -right-1 flex size-3">
                        <span class="bg-success absolute inline-flex h-full w-full rounded-full opacity-75"></span>
                        <span class="bg-success relative inline-flex size-3 rounded-full"></span>
                      </span>
                    </div>
                    <div>
                      <div class="flex items-center gap-2">
                        <h4 class="text-foreground text-sm font-semibold">Active AIS Sea Telemetry</h4>
                        <span ui-badge variant="secondary" class="bg-success/10 text-success text-xs">
                          Live Transponder
                        </span>
                      </div>
                      <p class="text-muted-foreground text-xs">
                        Vessel Ever Given navigating Red Sea corridor · Last ping:
                        <span class="text-foreground font-mono font-medium">12s ago</span>
                      </p>
                    </div>
                  </div>
    
                  <div class="flex flex-wrap items-center gap-3 font-mono text-xs">
                    <div class="border-border/80 bg-background/80 rounded-md border px-2.5 py-1.5">
                      <span class="text-muted-foreground">Speed: </span>
                      <span class="text-foreground font-semibold tabular-nums">18.4 knots</span>
                    </div>
                    <div class="border-border/80 bg-background/80 rounded-md border px-2.5 py-1.5">
                      <span class="text-muted-foreground">Heading: </span>
                      <span class="text-foreground font-semibold">315° NW</span>
                    </div>
                    <div class="border-border/80 bg-background/80 rounded-md border px-2.5 py-1.5">
                      <span class="text-muted-foreground">Coords: </span>
                      <span class="text-foreground font-semibold tabular-nums">24°18'N, 37°42'E</span>
                    </div>
                  </div>
                </div>
              </div>
    
              <!-- Connected Timeline Stepper -->
              <div class="relative pl-6 sm:pl-8">
                <!-- Vertical Track Line -->
                <div class="bg-border absolute top-3 bottom-3 left-3 w-0.5 -translate-x-1/2 sm:left-4"></div>
    
                <div class="space-y-6">
                  @for (milestone of filteredMilestones(); track milestone.id) {
                    <div class="group relative flex flex-col gap-2">
                      <!-- Step Node Circle -->
                      <div class="absolute -left-6 flex items-center justify-center sm:-left-8">
                        @if (milestone.status === 'completed') {
                          <div
                            class="ring-background bg-success bg-success flex size-6 items-center justify-center rounded-full text-white shadow-2xs ring-4 sm:size-8 dark:text-zinc-950"
                          >
                            <lucide-icon [img]="Check" class="size-3.5 stroke-[2.5] sm:size-4" aria-hidden="true" />
                          </div>
                        } @else if (milestone.status === 'current') {
                          <div
                            class="border-primary bg-background text-primary ring-background relative flex size-6 items-center justify-center rounded-full border-2 shadow-xs ring-4 sm:size-8"
                          >
                            <span class="relative flex size-2.5 sm:size-3">
                              <span class="bg-primary absolute inline-flex h-full w-full rounded-full opacity-75"></span>
                              <span class="bg-primary relative inline-flex size-2.5 rounded-full sm:size-3"></span>
                            </span>
                          </div>
                        } @else {
                          <div
                            class="border-border bg-muted/60 text-muted-foreground ring-background flex size-6 items-center justify-center rounded-full border ring-4 sm:size-8"
                          >
                            <span class="font-mono text-xs font-medium">{{ milestone.stepNumber }}</span>
                          </div>
                        }
                      </div>
    
                      <!-- Milestone Content Card -->
                      <div [class]="milestoneCardClass(milestone.status)">
                        <div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
                          <div class="space-y-1">
                            <div class="flex flex-wrap items-center gap-2">
                              <span class="text-foreground text-sm font-semibold">{{ milestone.title }}</span>
    
                              @if (milestone.status === 'completed') {
                                <span ui-badge variant="secondary" class="bg-success/10 text-success text-xs font-medium">
                                  Completed
                                </span>
                              } @else if (milestone.status === 'current') {
                                <span ui-badge class="bg-primary text-primary-foreground text-xs font-medium">
                                  Active Now
                                </span>
                              } @else {
                                <span ui-badge variant="outline" class="border-border text-muted-foreground text-xs">
                                  Upcoming
                                </span>
                              }
    
                              <span class="text-muted-foreground font-mono text-xs"> ({{ milestone.facility }}) </span>
                            </div>
    
                            <p class="text-muted-foreground text-xs">{{ milestone.description }}</p>
                          </div>
    
                          <div class="shrink-0 text-left sm:text-right">
                            <span class="text-foreground font-mono text-xs font-medium tabular-nums">
                              {{ milestone.date }}
                            </span>
                            <div class="text-muted-foreground mt-0.5 flex items-center gap-1 text-xs sm:justify-end">
                              <lucide-icon [img]="MapPin" class="size-3" aria-hidden="true" />
                              <span>{{ milestone.location }}</span>
                            </div>
                          </div>
                        </div>
    
                        @if (milestone.telemetry) {
                          <div
                            class="border-primary/20 bg-background/90 mt-3 grid grid-cols-2 gap-2 rounded-lg border p-2.5 font-mono text-xs sm:grid-cols-4"
                          >
                            <div>
                              <span class="text-muted-foreground">Speed: </span>
                              <span class="text-foreground font-semibold tabular-nums">{{
                                milestone.telemetry!.speed
                              }}</span>
                            </div>
                            <div>
                              <span class="text-muted-foreground">Heading: </span>
                              <span class="text-foreground font-semibold">{{ milestone.telemetry!.heading }}</span>
                            </div>
                            <div>
                              <span class="text-muted-foreground">Water Depth: </span>
                              <span class="text-foreground font-semibold tabular-nums">{{
                                milestone.telemetry!.depth
                              }}</span>
                            </div>
                            <div>
                              <span class="text-muted-foreground">AIS Status: </span>
                              <span class="text-success font-semibold">Broadcasting</span>
                            </div>
                          </div>
                        }
                      </div>
                    </div>
                  }
                </div>
              </div>
            </div>
          </div>
    
          <!-- 2-Column Grid: Specifications & Documentation Manifest -->
          <div class="grid grid-cols-1 gap-6 lg:grid-cols-12">
            <!-- Container Specifications & Cold Chain Card -->
            <div class="space-y-6 lg:col-span-7">
              <div ui-card class="border-border bg-card shadow-xs">
                <div ui-card-header class="pb-4">
                  <div class="flex items-center justify-between">
                    <div class="flex items-center gap-2">
                      <lucide-icon [img]="Box" class="text-primary size-4.5" aria-hidden="true" />
                      <h3 ui-card-title class="text-base font-semibold">Container & Cold Chain Specifications</h3>
                    </div>
                    <span ui-badge variant="outline" class="font-mono text-xs">ISO 6346 Verified</span>
                  </div>
                  <p ui-card-description class="text-xs">
                    Physical parameters, high-security seal integrity, and live Reefer temperature control log.
                  </p>
                </div>
                <div ui-card-content class="space-y-5">
                  <!-- Key Metric Chips -->
                  <div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
                    <div class="border-border bg-muted/30 rounded-lg border p-3">
                      <span class="text-muted-foreground text-xs">Gross Weight</span>
                      <p class="text-foreground mt-0.5 font-mono text-sm font-bold tabular-nums">24,500 kg</p>
                      <span class="text-muted-foreground text-xs">Max: 30,480 kg</span>
                    </div>
                    <div class="border-border bg-muted/30 rounded-lg border p-3">
                      <span class="text-muted-foreground text-xs">Bolt Seal #</span>
                      <p class="text-foreground mt-0.5 font-mono text-sm font-bold">SL-94021</p>
                      <span class="text-success text-xs">ISO 17712 High Sec</span>
                    </div>
                    <div class="border-border bg-muted/30 rounded-lg border p-3">
                      <span class="text-muted-foreground text-xs">Temperature</span>
                      <p class="text-foreground mt-0.5 font-mono text-sm font-bold tabular-nums">-18.0°C</p>
                      <span class="text-success text-xs">Actual: -18.2°C</span>
                    </div>
                    <div class="border-border bg-muted/30 rounded-lg border p-3">
                      <span class="text-muted-foreground text-xs">Humidity & Vent</span>
                      <p class="text-foreground mt-0.5 font-mono text-sm font-bold tabular-nums">85% RH</p>
                      <span class="text-muted-foreground text-xs">Vent: Closed (0 cbm)</span>
                    </div>
                  </div>
    
                  <ui-separator />
    
                  <!-- Detailed Specifications Table / List -->
                  <div class="grid grid-cols-1 gap-4 text-xs sm:grid-cols-2">
                    <div class="space-y-2.5">
                      <div class="flex items-center justify-between">
                        <span class="text-muted-foreground">Equipment Size:</span>
                        <span class="text-foreground font-medium">40ft High Cube Reefer (40HR)</span>
                      </div>
                      <div class="flex items-center justify-between">
                        <span class="text-muted-foreground">Tare Weight:</span>
                        <span class="text-foreground font-mono font-medium tabular-nums">3,300 kg</span>
                      </div>
                      <div class="flex items-center justify-between">
                        <span class="text-muted-foreground">Net Cargo Weight:</span>
                        <span class="text-foreground font-mono font-medium tabular-nums">21,200 kg</span>
                      </div>
                      <div class="flex items-center justify-between">
                        <span class="text-muted-foreground">Cargo Volume:</span>
                        <span class="text-foreground font-mono font-medium tabular-nums">67.3 CBM</span>
                      </div>
                    </div>
    
                    <div class="space-y-2.5">
                      <div class="flex items-center justify-between">
                        <span class="text-muted-foreground">Reefer Unit:</span>
                        <span class="text-foreground font-medium">Carrier Transicold PrimeLINE</span>
                      </div>
                      <div class="flex items-center justify-between">
                        <span class="text-muted-foreground">Power Source:</span>
                        <span class="text-foreground font-medium">Vessel 440V 3-Phase Plug</span>
                      </div>
                      <div class="flex items-center justify-between">
                        <span class="text-muted-foreground">Defrost Cycle:</span>
                        <span class="text-foreground font-mono font-medium">Auto-Defrost (Every 12h)</span>
                      </div>
                      <div class="flex items-center justify-between">
                        <span class="text-muted-foreground">Cold-Chain Compliance:</span>
                        <span class="text-success font-semibold">HACCP / GDP Certified</span>
                      </div>
                    </div>
                  </div>
    
                  <!-- Cold Chain Live Sensor Gauge Banner -->
                  <div
                    class="border-border/80 bg-muted/40 flex flex-col gap-3 rounded-lg border p-3 text-xs sm:flex-row sm:items-center sm:justify-between"
                  >
                    <div class="flex items-center gap-2">
                      <lucide-icon [img]="Thermometer" class="text-primary size-4" aria-hidden="true" />
                      <span class="text-foreground font-medium">Continuous Temperature Telemetry:</span>
                      <span class="text-muted-foreground font-mono">24-hour variance ±0.3°C (Nominal)</span>
                    </div>
                    <div class="text-success flex items-center gap-1.5 font-medium">
                      <lucide-icon [img]="ShieldCheck" class="size-3.5" aria-hidden="true" />
                      <span>Zero Excursions</span>
                    </div>
                  </div>
                </div>
              </div>
            </div>
    
            <!-- Commercial Documents & Shipping Manifest Card -->
            <div class="space-y-6 lg:col-span-5">
              <div ui-card class="border-border bg-card shadow-xs">
                <div ui-card-header class="pb-4">
                  <div class="flex items-center justify-between">
                    <div class="flex items-center gap-2">
                      <lucide-icon [img]="FileText" class="text-primary size-4.5" aria-hidden="true" />
                      <h3 ui-card-title class="text-base font-semibold">Shipping Documentation</h3>
                    </div>
                    <span ui-badge variant="secondary" class="text-xs">Customs Released</span>
                  </div>
                  <p ui-card-description class="text-xs">
                    Bill of Lading, invoice identifiers, and trade compliance references.
                  </p>
                </div>
                <div ui-card-content class="space-y-4">
                  <div class="space-y-2.5 text-xs">
                    <div class="border-border/60 bg-muted/30 flex items-center justify-between rounded-lg border p-2.5">
                      <span class="text-muted-foreground">Commercial Invoice #:</span>
                      <span class="text-foreground font-mono font-bold">CI-2026-849</span>
                    </div>
                    <div class="border-border/60 bg-muted/30 flex items-center justify-between rounded-lg border p-2.5">
                      <span class="text-muted-foreground">Master Bill of Lading (MBL):</span>
                      <span class="text-foreground font-mono font-bold">MAEU-98421094</span>
                    </div>
                    <div class="border-border/60 bg-muted/30 flex items-center justify-between rounded-lg border p-2.5">
                      <span class="text-muted-foreground">Booking Reference:</span>
                      <span class="text-foreground font-mono font-medium">BKG-7712093</span>
                    </div>
                    <div class="border-border/60 bg-muted/30 flex items-center justify-between rounded-lg border p-2.5">
                      <span class="text-muted-foreground">Incoterms 2020:</span>
                      <span class="text-foreground font-medium">FOB Shanghai Port</span>
                    </div>
                  </div>
    
                  <ui-separator />
    
                  <!-- Parties: Shipper and Consignee -->
                  <div class="space-y-3 text-xs">
                    <div>
                      <p class="text-muted-foreground font-medium">Shipper / Exporter</p>
                      <p class="text-foreground font-semibold">Ningbo Apex International Marine Trade Co.</p>
                      <p class="text-muted-foreground">Beilun District, Ningbo, Zhejiang, China</p>
                    </div>
                    <div>
                      <p class="text-muted-foreground font-medium">Consignee / Importer</p>
                      <p class="text-foreground font-semibold">EuroHub Logistics & Cold Storage GmbH</p>
                      <p class="text-muted-foreground">Logport I, 47226 Duisburg, Germany</p>
                    </div>
                  </div>
    
                  <div class="flex flex-col items-center gap-2 pt-2 sm:flex-row">
                    <button
                      ui-button
                      aria-label="Download attachment"
                      variant="outline"
                      size="sm"
                      class="min-w-0 justify-center gap-2 text-xs shadow-xs sm:flex-1"
                    >
                      <lucide-icon [img]="Download" class="size-3.5" aria-hidden="true" />
                      Download Bill of Lading
                    </button>
                    <button
                      ui-button
                      variant="ghost"
                      size="sm"
                      class="text-muted-foreground hover:text-foreground min-w-0 justify-center gap-2 text-xs sm:flex-1"
                    >
                      <lucide-icon [img]="FileCheck2" class="size-3.5" aria-hidden="true" />
                      Customs Pack (PDF)
                    </button>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiShipmentTrackingTimelineComponent {
      protected readonly Anchor = Anchor
      protected readonly ArrowRight = ArrowRight
      protected readonly Box = Box
      protected readonly CalendarClock = CalendarClock
      protected readonly Check = Check
      protected readonly Compass = Compass
      protected readonly Copy = Copy
      protected readonly Download = Download
      protected readonly FileCheck2 = FileCheck2
      protected readonly FileText = FileText
      protected readonly MapPin = MapPin
      protected readonly Navigation = Navigation
      protected readonly Radio = Radio
      protected readonly Search = Search
      protected readonly Share2 = Share2
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Ship = Ship
      protected readonly Thermometer = Thermometer
      protected readonly Timer = Timer
    
      @Input('initialContainerId') set initialContainerIdInput(v: string | undefined) {
        if (v !== undefined) this.searchQuery.set(v)
      }
      @Input('initialFilter') set initialFilterInput(v: MilestoneFilter | undefined) {
        if (v) this.activeFilter.set(v)
      }
      @Input('class') className?: string
    
      readonly searchQuery = signal('MSKU-9482014')
      readonly activeFilter = signal<MilestoneFilter>('all')
      readonly copiedContainerId = signal(false)
      readonly copiedBLLink = signal(false)
    
      readonly filteredMilestones = computed(() => {
        const filter = this.activeFilter()
        if (filter === 'completed') {
          return defaultMilestones.filter((m) => m.status === 'completed')
        }
        if (filter === 'upcoming') {
          return defaultMilestones.filter((m) => m.status === 'upcoming')
        }
        return defaultMilestones
      })
    
      get rootClass(): string {
        return cn('w-full space-y-6', this.className)
      }
    
      filterButtonClass(filter: MilestoneFilter): string {
        // [class] overwrites the whole class attribute: the base layout classes must be merged here.
        return cn(
          'rounded-md px-2.5 py-1 font-medium transition-colors',
          this.activeFilter() === filter
            ? 'bg-background text-foreground font-semibold shadow-xs'
            : 'text-muted-foreground hover:text-foreground',
        )
      }
    
      milestoneCardClass(status: MilestoneStatus): string {
        return cn(
          'rounded-xl border p-4 transition-colors',
          status === 'current'
            ? 'border-primary/50 bg-primary/5 ring-primary/20 shadow-xs ring-1'
            : status === 'completed'
              ? 'border-border/80 bg-card/60 hover:bg-muted/30'
              : 'border-border/60 bg-muted/20 opacity-80 hover:opacity-100',
        )
      }
    
      copyContainerId(): void {
        if (typeof navigator !== 'undefined' && navigator.clipboard) {
          navigator.clipboard.writeText('MSKU-9482014')
          this.copiedContainerId.set(true)
          setTimeout(() => {
            this.copiedContainerId.set(false)
          }, 2000)
        }
      }
    
      copyTrackingLink(): void {
        if (typeof navigator !== 'undefined' && navigator.clipboard) {
          navigator.clipboard.writeText('https://uipkge.dev/track/MSKU-9482014')
          this.copiedBLLink.set(true)
          setTimeout(() => {
            this.copiedBLLink.set(false)
          }, 2000)
        }
      }
    }
    
  • src/app/components/blocks/shipment-tracking-timeline/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/shipment-tracking-timeline.json