Menu

Change language

GitHub repo

How It Works — Vertical Timeline

blockmarketing

Vertical numbered timeline with a continuous rail, per-stage status badge, outcome bullets, and a paired detail card — for onboarding or implementation narratives that need more room than a three-up row.

Installation

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

Variants

Loading interactive previews…

Schema

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

TimelineStage
interface TimelineStage {
  title: string
  status: string
  done: boolean
  body: string
  outcomes: string[]
}

Files installed (2)

  • src/app/components/blocks/how-it-works-timeline/how-it-works-timeline.component.ts6.3 kB
    // Vertical numbered timeline with a continuous rail, per-stage status badge, outcome
    // bullets, and a paired detail card — for onboarding or implementation narratives that
    // need more room than a three-up row. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
    import { Check, Clock, LucideAngularModule } 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,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import {
      UiTimelineComponent,
      UiTimelineContentComponent,
      UiTimelineDescriptionComponent,
      UiTimelineHeaderComponent,
      UiTimelineItemComponent,
      UiTimelineMediaComponent,
      UiTimelineTitleComponent,
    } from '@/ui/timeline/timeline.component'
    
    export interface TimelineStage {
      title: string
      status: string
      done: boolean
      body: string
      outcomes: string[]
    }
    
    const stages: TimelineStage[] = [
      {
        title: 'Kickoff & access',
        status: 'Week 1',
        done: true,
        body: 'A shared channel, a named engineer, and read-only credentials. Nothing is provisioned until scope is signed off.',
        outcomes: ['Workspace provisioned', 'SSO connected', 'Data contract agreed'],
      },
      {
        title: 'Model your data',
        status: 'Week 2',
        done: true,
        body: 'We mirror your warehouse schema and reconcile it against last quarter’s numbers before anything is published.',
        outcomes: ['Schema mirrored', 'Historical backfill', 'Reconciliation signed off'],
      },
      {
        title: 'Pilot with one team',
        status: 'Week 3–4',
        done: false,
        body: 'A single team runs their real reporting cycle on it. We fix what they hit before anyone else sees it.',
        outcomes: ['Pilot cohort live', 'Feedback loop open', 'Runbook drafted'],
      },
      {
        title: 'Roll out & hand over',
        status: 'Week 5',
        done: false,
        body: 'Permissions, alerting, and the runbook move to your team. Support stays on call through the first close.',
        outcomes: ['Org-wide access', 'Alerting configured', 'Ownership transferred'],
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-how-it-works-timeline, [ui-how-it-works-timeline]',
      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,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiSeparatorComponent,
        UiTimelineComponent,
        UiTimelineContentComponent,
        UiTimelineDescriptionComponent,
        UiTimelineHeaderComponent,
        UiTimelineItemComponent,
        UiTimelineMediaComponent,
        UiTimelineTitleComponent,
      ],
      template: `
        <section data-slot="how-it-works-timeline" [class]="rootClass">
          <div class="mx-auto max-w-5xl px-6 py-20 lg:py-28">
            <div class="max-w-2xl">
              <span ui-badge variant="secondary">Implementation</span>
              <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">
                From kickoff to hand-over in five weeks
              </h2>
              <p class="text-muted-foreground mt-3 text-lg">
                The same plan we run for every rollout. No phase starts before the one above it is signed off.
              </p>
            </div>
    
            <ui-timeline class="mt-14" density="comfortable">
              @for (stage of allStages; track stage.title; let index = $index) {
                <ui-timeline-item [status]="stage.done ? 'success' : 'default'">
                  <ui-timeline-media variant="outline" [status]="stage.done ? 'success' : 'muted'">
                    @if (stage.done) {
                      <lucide-icon [img]="Check" class="size-4" aria-hidden="true" />
                    } @else {
                      <span class="font-mono text-xs font-semibold">{{ pad(index + 1) }}</span>
                    }
                  </ui-timeline-media>
    
                  <ui-timeline-content>
                    <ui-timeline-header>
                      <ui-timeline-title as="h3" class="text-base font-semibold">{{ stage.title }}</ui-timeline-title>
                      <span ui-badge [variant]="stage.done ? 'secondary' : 'outline'" class="gap-1.5 font-normal">
                        <lucide-icon [img]="Clock" class="size-3" aria-hidden="true" />
                        {{ stage.status }}
                      </span>
                    </ui-timeline-header>
    
                    <ui-timeline-description class="mt-2 max-w-prose leading-relaxed">
                      {{ stage.body }}
                    </ui-timeline-description>
    
                    <div ui-card class="mt-4">
                      <div ui-card-header class="pb-3">
                        <h4 ui-card-title class="text-muted-foreground text-xs font-medium tracking-wide uppercase">
                          What you have at the end
                        </h4>
                      </div>
                      <div ui-card-content>
                        <ul class="grid gap-2 sm:grid-cols-3">
                          @for (outcome of stage.outcomes; track outcome) {
                            <li class="text-foreground flex items-start gap-2 text-sm">
                              <lucide-icon [img]="Check" class="text-primary mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
                              <span>{{ outcome }}</span>
                            </li>
                          }
                        </ul>
                      </div>
                    </div>
                  </ui-timeline-content>
                </ui-timeline-item>
              }
            </ui-timeline>
    
            <ui-separator />
            <div class="mt-8 flex flex-wrap items-center gap-4">
              <button ui-button>Book an implementation call</button>
              <button ui-button variant="ghost">Download the rollout plan</button>
            </div>
          </div>
        </section>
      `,
    })
    export class UiHowItWorksTimelineComponent {
      protected readonly Check = Check
      protected readonly Clock = Clock
      protected readonly allStages = stages
    
      @Input('class') className?: string
    
      get rootClass(): string {
        return cn('bg-background', this.className)
      }
    
      pad(n: number): string {
        return String(n).padStart(2, '0')
      }
    }
    
  • src/app/components/blocks/how-it-works-timeline/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/how-it-works-timeline.json