Menu

Change language

GitHub repo

How It Works — Tabbed Steps

blockmarketing

Step explainer where a vertical tab list of stages swaps a paired detail panel showing the action, the expected result, and a primitive-built screen for that step.

Installation

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

Variants

Loading interactive previews…

Schema

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

TabbedStepRow
interface TabbedStepRow {
  name: string
  detail: string
}
TabbedStep
interface TabbedStep {
  id: string
  label: string
  action: string
  result: string
  rows: TabbedStepRow[]
}

Files installed (2)

  • src/app/components/blocks/how-it-works-tabbed-steps/how-it-works-tabbed-steps.component.ts6.8 kB
    // Step explainer where a vertical tab list of stages swaps a paired detail panel showing
    // the action, the expected result, and a primitive-built screen for that step.
    // Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core'
    import { Check, 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 { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import {
      UiTabsComponent,
      UiTabsContentComponent,
      UiTabsListComponent,
      UiTabsTriggerComponent,
    } from '@/ui/tabs/tabs.component'
    
    export interface TabbedStepRow {
      name: string
      detail: string
    }
    
    export interface TabbedStep {
      id: string
      label: string
      action: string
      result: string
      rows: TabbedStepRow[]
    }
    
    const steps: TabbedStep[] = [
      {
        id: 'connect',
        label: 'Connect',
        action: 'Point us at the warehouse with a read-only role.',
        result: 'Schema mirrored, 412 tables catalogued, nothing copied out.',
        rows: [
          { name: 'warehouse.orders', detail: '2.4M rows' },
          { name: 'warehouse.refunds', detail: '88k rows' },
          { name: 'warehouse.fx_rates', detail: 'daily' },
        ],
      },
      {
        id: 'define',
        label: 'Define',
        action: 'Author the metric as a file and open a pull request.',
        result: 'Reviewed by its owner, with a diff and a revert path.',
        rows: [
          { name: 'revenue_net', detail: 'finance-analytics' },
          { name: 'margin_by_channel', detail: 'finance-analytics' },
          { name: 'pipeline_weighted', detail: 'revops' },
        ],
      },
      {
        id: 'certify',
        label: 'Certify',
        action: 'Reconcile the definition against the prior four quarters.',
        result: 'Sign-off recorded; the metric can now be published.',
        rows: [
          { name: 'Q4 2025', detail: 'matched' },
          { name: 'Q3 2025', detail: 'matched' },
          { name: 'Q2 2025', detail: 'matched' },
        ],
      },
      {
        id: 'publish',
        label: 'Publish',
        action: 'Release to every consumer at once.',
        result: 'Dashboards, exports, and the API resolve the same number.',
        rows: [
          { name: 'Exec summary', detail: 'live' },
          { name: 'Finance close pack', detail: 'live' },
          { name: 'Partner API', detail: 'live' },
        ],
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-how-it-works-tabbed-steps, [ui-how-it-works-tabbed-steps]',
      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,
        UiSeparatorComponent,
        UiTabsComponent,
        UiTabsContentComponent,
        UiTabsListComponent,
        UiTabsTriggerComponent,
      ],
      template: `
        <section data-slot="how-it-works-tabbed-steps" [class]="rootClass">
          <div class="mx-auto max-w-6xl px-6 py-20 lg:py-28">
            <div class="max-w-2xl">
              <span ui-badge variant="secondary">How it works</span>
              <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Pick a stage to see what it produces</h2>
            </div>
    
            <ui-tabs [value]="activeStep()" (valueChange)="setActiveStep($event)" orientation="vertical" class="mt-10">
              <div class="grid gap-8 lg:grid-cols-[16rem_1fr] lg:gap-12">
                <!-- Vertical tab rail: orientation is set on both list and triggers so
                     arrow keys move up and down rather than left and right. -->
                <ui-tabs-list variant="underline" orientation="vertical" class="h-auto w-full flex-col items-stretch">
                  @for (step of allSteps; track step.id; let index = $index) {
                    <button
                      ui-tabs-trigger
                      [value]="step.id"
                      variant="underline"
                      orientation="vertical"
                      class="justify-start gap-3"
                    >
                      <span class="text-muted-foreground font-mono text-xs">{{ pad(index + 1) }}</span>
                      {{ step.label }}
                    </button>
                  }
                </ui-tabs-list>
    
                @for (step of allSteps; track step.id) {
                  <ui-tabs-content [value]="step.id" class="mt-0">
                    <!-- min-h keeps the panel from collapsing between stages. -->
                    <div class="grid min-h-[20rem] gap-6 sm:grid-cols-2">
                      <div>
                        <p class="text-muted-foreground text-sm">You do</p>
                        <p class="mt-1.5 text-lg leading-snug font-medium text-balance">{{ step.action }}</p>
    
                        <ui-separator class="my-5" />
    
                        <p class="text-muted-foreground text-sm">You get</p>
                        <p class="mt-1.5 flex items-start gap-2 text-sm leading-relaxed">
                          <lucide-icon [img]="Check" class="text-primary mt-0.5 size-4 shrink-0" aria-hidden="true" />
                          <span>{{ step.result }}</span>
                        </p>
    
                        <button ui-button variant="link" class="mt-5 h-auto p-0">Read this step in the docs</button>
                      </div>
    
                      <div ui-card class="self-start">
                        <div ui-card-content class="p-0">
                          <div class="border-border text-muted-foreground border-b px-4 py-2.5 font-mono text-xs">
                            {{ step.label }}
                          </div>
                          <ul class="divide-border divide-y">
                            @for (row of step.rows; track row.name) {
                              <li class="flex items-center justify-between gap-4 px-4 py-2.5">
                                <span class="min-w-0 truncate font-mono text-xs">{{ row.name }}</span>
                                <span class="text-muted-foreground shrink-0 text-xs">{{ row.detail }}</span>
                              </li>
                            }
                          </ul>
                        </div>
                      </div>
                    </div>
                  </ui-tabs-content>
                }
              </div>
            </ui-tabs>
          </div>
        </section>
      `,
    })
    export class UiHowItWorksTabbedStepsComponent {
      protected readonly Check = Check
      protected readonly allSteps = steps
    
      @Input() set defaultStep(v: string) {
        if (steps.some((step) => step.id === v)) this.activeStep.set(v)
      }
      @Input('class') className?: string
    
      readonly activeStep = signal('connect')
    
      get rootClass(): string {
        return cn('bg-background', this.className)
      }
    
      pad(n: number): string {
        return String(n).padStart(2, '0')
      }
    
      setActiveStep(value: string): void {
        if (steps.some((step) => step.id === value)) this.activeStep.set(value)
      }
    }
    
  • src/app/components/blocks/how-it-works-tabbed-steps/index.ts0.1 kB

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