Menu

Change language

GitHub repo

Demo — Guided Tour

blockmarketing

Step-through product tour where next and previous move a highlight across a mocked interface, with a step counter and a skip control.

Installation

$npx uipkge-ng@latest add demo-guided-tour
Manifest:https://uipkge.dev/r/angular/demo-guided-tour.jsonInstalls to:src/app/components/blocks/demo-guided-tour/

Variants

Loading interactive previews…

Schema

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

GuidedTourStep
interface GuidedTourStep {
  target: string
  title: string
  body: string
}
GuidedTourPanel
interface GuidedTourPanel {
  id: string
  label: string
  rows: string[]
}

Files installed (2)

  • src/app/components/blocks/demo-guided-tour/demo-guided-tour.component.ts6 kB
    // Step-through product tour where next and previous move a highlight across a mocked
    // interface, with a step counter, progress bar and a skip control. Port of the React
    // block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import { ArrowLeft, ArrowRight, 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 } from '@/ui/card/card.component'
    import { UiProgressComponent } from '@/ui/progress/progress.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    
    export interface GuidedTourStep {
      target: string
      title: string
      body: string
    }
    
    export interface GuidedTourPanel {
      id: string
      label: string
      rows: string[]
    }
    
    // Each step names the panel it highlights, so the copy and the mock cannot
    // drift apart when someone reorders the tour.
    export const GUIDED_TOUR_STEPS: GuidedTourStep[] = [
      {
        target: 'catalog',
        title: 'Start from the catalogue',
        body: 'Every table we mirrored, with its freshness. Pick what the metric reads from.',
      },
      {
        target: 'definition',
        title: 'Write the definition',
        body: 'The window, the owner, and the joins. This is the file that opens a pull request.',
      },
      {
        target: 'consumers',
        title: 'See who it affects',
        body: 'Before publishing, the consumer list shows every dashboard and export that resolves it.',
      },
      {
        target: 'history',
        title: 'Check the history',
        body: 'Every prior version with its reviewer, and a one-click revert to any of them.',
      },
    ]
    
    export const GUIDED_TOUR_PANELS: GuidedTourPanel[] = [
      { id: 'catalog', label: 'Catalogue', rows: ['warehouse.orders', 'warehouse.refunds', 'warehouse.fx_rates'] },
      {
        id: 'definition',
        label: 'revenue_net.yml',
        rows: ['window: trailing_28d', 'owner: finance-analytics', 'joins: refunds, fx'],
      },
      { id: 'consumers', label: 'Consumers', rows: ['Exec summary', 'Finance close pack', 'Partner API'] },
      {
        id: 'history',
        label: 'History',
        rows: ['v128 · 14 Feb · A. Reyes', 'v127 · 02 Jan · A. Reyes', 'v126 · 11 Dec · M. Ellery'],
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-demo-guided-tour, [ui-demo-guided-tour]',
      standalone: true,
      host: {
        '[attr.data-slot]': '"demo-guided-tour"',
        '[class]': 'hostClass',
      },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiProgressComponent,
        UiSeparatorComponent,
      ],
      template: `
        <div class="mx-auto max-w-5xl px-6 py-20 lg:py-28">
          <div class="flex flex-wrap items-end justify-between gap-6">
            <div class="max-w-xl">
              <span ui-badge variant="secondary">Guided tour</span>
              <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Four screens, ninety seconds</h2>
            </div>
            <button ui-button variant="ghost" size="sm">Skip and just read the docs</button>
          </div>
    
          <div class="mt-10 grid gap-6 lg:grid-cols-[1fr_1.3fr] lg:gap-10">
            <div>
              <div ui-progress [value]="progress()" class="h-1" aria-label="Tour progress"></div>
              <p class="text-muted-foreground mt-3 font-mono text-xs">Step {{ index() + 1 }} of {{ steps.length }}</p>
    
              <!-- min-h holds the copy block at its tallest so the controls do not
                   move between steps. -->
              <div class="min-h-[7rem]">
                <h3 class="mt-4 text-xl font-semibold tracking-tight">{{ current().title }}</h3>
                <p class="text-muted-foreground mt-2 leading-relaxed">{{ current().body }}</p>
              </div>
    
              <div class="mt-5 flex items-center gap-2">
                <button ui-button variant="outline" size="sm" [disabled]="index() === 0" (click)="back()">
                  <lucide-icon [img]="ArrowLeft" class="mr-1.5 size-3.5" />
                  Back
                </button>
                <button ui-button size="sm" [disabled]="index() === steps.length - 1" (click)="next()">
                  Next
                  <lucide-icon [img]="ArrowRight" class="ml-1.5 size-3.5" />
                </button>
              </div>
            </div>
    
            <div ui-card>
              <div ui-card-content class="space-y-2 p-4">
                @for (panel of panels; track panel.id) {
                  <div class="rounded-lg border p-3 transition-colors" [class]="panelClass(panel.id)">
                    <div class="flex items-center justify-between gap-3">
                      <p class="font-mono text-xs font-medium">{{ panel.label }}</p>
                      @if (panel.id === current().target) {
                        <span ui-badge variant="secondary">This step</span>
                      }
                    </div>
                    <ui-separator class="my-2" />
                    <ul class="space-y-1">
                      @for (row of panel.rows; track row) {
                        <li class="text-muted-foreground truncate font-mono text-xs">
                          {{ row }}
                        </li>
                      }
                    </ul>
                  </div>
                }
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiDemoGuidedTourComponent {
      protected readonly ArrowLeft = ArrowLeft
      protected readonly ArrowRight = ArrowRight
    
      @Input('class') className?: string
    
      readonly steps = GUIDED_TOUR_STEPS
      readonly panels = GUIDED_TOUR_PANELS
    
      readonly index = signal(0)
      readonly current = computed(() => this.steps[this.index()] ?? this.steps[0]!)
      readonly progress = computed(() => ((this.index() + 1) / this.steps.length) * 100)
    
      get hostClass(): string {
        return cn('bg-background block w-full', this.className)
      }
    
      panelClass(id: string): string {
        return cn(id === this.current().target ? 'border-primary bg-muted/40' : 'border-border bg-background opacity-60')
      }
    
      next(): void {
        this.index.update((i) => Math.min(i + 1, this.steps.length - 1))
      }
    
      back(): void {
        this.index.update((i) => Math.max(i - 1, 0))
      }
    }
    
  • src/app/components/blocks/demo-guided-tour/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/demo-guided-tour.json