Onboarding Wizard
blockauthMulti-step workspace setup flow in a SectionCard: a four-step Stepper (Workspace → Team → Preferences → Done) with check icons on completed steps, a workspace-name input with live URL slug preview, teammate invites as removable email chips with a skip link, preference Switch rows, and a success panel with a glowing check circle. Footer carries Back/Continue actions plus a Step X of 4 counter. Data is stubbed inline — wire your API of choice.
Installation
$pnpm dlx uipkge-ng@latest add onboarding-wizard$npx uipkge-ng@latest add onboarding-wizard$yarn dlx uipkge-ng@latest add onboarding-wizard$bunx uipkge-ng@latest add onboarding-wizardManifest:
https://uipkge.dev/r/angular/onboarding-wizard.jsonInstalls to:src/app/components/blocks/onboarding-wizard/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
Preferenceinterface Preference {
id: string
label: string
description: string
enabled: boolean
}npm dependencies
Files installed (2)
src/app/components/blocks/onboarding-wizard/onboarding-wizard.component.ts10.6 kB
import { Component, Input, OnInit, computed, signal, ChangeDetectionStrategy } from '@angular/core' import { Check, ChevronLeft, ChevronRight, Globe, LucideAngularModule, Plus, X } from 'lucide-angular' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSectionCardComponent } from '@/ui/section-card/section-card.component' import { UiStepperComponent, type StepperStep } from '@/ui/stepper/stepper.component' import { UiSwitchComponent } from '@/ui/switch/switch.component' interface Preference { id: string label: string description: string enabled: boolean } const steps: StepperStep[] = [ { id: 1, title: 'Workspace' }, { id: 2, title: 'Team' }, { id: 3, title: 'Preferences' }, { id: 4, title: 'Done' }, ] const defaultPreferences: Preference[] = [ { id: 'digests', label: 'Email digests', description: 'A daily summary of workspace activity.', enabled: true, }, { id: 'updates', label: 'Product updates', description: 'Occasional notes about new features and improvements.', enabled: false, }, { id: 'reports', label: 'Weekly reports', description: 'Usage analytics delivered every Monday morning.', enabled: true, }, ] const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ /** * Angular port of the React OnboardingWizard block: a SectionCard with a four-step Stepper * (Workspace, Team, Preferences, Done), a workspace-name input with a live URL slug, teammate * invites as removable chips, preference Switch rows and a success panel. Backward navigation * through the stepper is free; forward goes through the footer button only. * * Icons inside `ui-button` sit in `<lucide-icon>`, so the React `size="sm"` `has-[>svg]:px-2.5` * padding is set explicitly on those buttons. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-onboarding-wizard, [ui-onboarding-wizard]', standalone: true, // React renders the SectionCard itself: the host stays out of layout and `class` goes to the card. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiInputComponent, UiSectionCardComponent, UiStepperComponent, UiSwitchComponent, ], template: ` <ui-section-card data-slot="onboarding-wizard" [title]="'Set up your workspace'" [description]="'Four quick steps and your team is ready to collaborate.'" [class]="className" > <ui-stepper [steps]="steps" [value]="step()" (valueChange)="onStepperInput($event)" class="mb-6" /> @if (step() === 1) { <div class="space-y-4"> <div class="space-y-2"> <label for="onboarding-workspace-name" class="text-sm font-medium">Workspace name</label> <ui-input id="onboarding-workspace-name" [value]="workspaceName()" (valueChange)="workspaceName.set($event)" placeholder="Acme Inc" autoComplete="off" /> </div> <div class="border-border bg-muted/40 flex items-center gap-2 rounded-md border px-3 py-2"> <lucide-icon [img]="Globe" class="text-muted-foreground size-4 shrink-0" aria-hidden="true" /> <p class="min-w-0 truncate text-xs"> <span class="text-muted-foreground">uipkge.dev/</span> <span class="text-foreground font-medium">{{ slug() }}</span> </p> </div> </div> } @if (step() === 2) { <div class="space-y-4"> <form class="flex gap-2" (submit)="onInviteSubmit($event)"> <ui-input [value]="inviteDraft()" (valueChange)="onInviteDraft($event)" type="email" placeholder="[email protected]" [status]="inviteInvalid() ? 'error' : undefined" class="flex-1" /> <button ui-button type="submit" variant="outline" size="sm" class="shrink-0 px-2.5"> <lucide-icon [img]="Plus" aria-hidden="true" /> Add </button> </form> @if (inviteInvalid()) { <p role="alert" class="text-destructive text-xs">Enter a valid email address.</p> } @if (invites().length > 0) { <div class="flex flex-wrap gap-2"> @for (email of invites(); track email) { <span ui-badge variant="secondary" class="gap-1 py-1 pr-1 pl-2.5" >{{ email }}<button type="button" [attr.aria-label]="'Remove ' + email" class="hover:bg-foreground/10 focus-visible:ring-ring min-h-6 rounded-full p-0.5 transition-colors focus-visible:ring-2 focus-visible:outline-none" (click)="removeInvite(email)" > <lucide-icon [img]="X" class="size-3" aria-hidden="true" /> </button> </span> } </div> } @else { <p class="text-muted-foreground text-xs">No teammates invited yet.</p> } <button type="button" class="text-muted-foreground hover:text-foreground focus-visible:ring-ring min-h-6 text-xs underline-offset-4 transition-colors hover:underline focus-visible:ring-2 focus-visible:outline-none" (click)="goNext()" > Skip for now </button> </div> } @if (step() === 3) { <div class="divide-y rounded-lg border"> @for (pref of preferences(); track pref.id) { <div class="flex items-center justify-between gap-4 p-4"> <div class="min-w-0"> <p class="text-sm font-medium">{{ pref.label }}</p> <p class="text-muted-foreground mt-0.5 text-xs">{{ pref.description }}</p> </div> <button ui-switch [attr.aria-label]="pref.label" [checked]="pref.enabled" (checkedChange)="setPreference(pref.id, $event)" ></button> </div> } </div> } @if (step() === 4) { <div class="space-y-4 py-6 text-center"> <div class="border-success/30 bg-success/10 text-success mx-auto flex size-16 items-center justify-center rounded-full border shadow-xs" > <lucide-icon [img]="Check" class="size-8" aria-hidden="true" /> </div> <div> <p class="text-lg font-semibold">You're all set</p> <p class="text-muted-foreground mx-auto mt-1 max-w-sm text-sm">{{ doneMessage() }}</p> </div> <button ui-button variant="outline" size="sm" (click)="reset()">Set up another workspace</button> </div> } <div slot="footer" class="flex w-full items-center justify-between"> @if (step() > 1 && step() < 4) { <button ui-button variant="ghost" size="sm" class="px-2.5" (click)="step.set(step() - 1)"> <lucide-icon [img]="ChevronLeft" aria-hidden="true" /> Back </button> } @else { <span aria-hidden="true"></span> } <span class="text-muted-foreground text-xs">Step {{ step() }} of 4</span> @if (step() < 4) { <button ui-button size="sm" class="px-2.5" [disabled]="step() === 1 && workspaceName().trim() === ''" (click)="goNext()" > {{ step() === 3 ? 'Finish' : 'Continue' }} <lucide-icon [img]="ChevronRight" aria-hidden="true" /> </button> } @else { <span aria-hidden="true"></span> } </div> </ui-section-card> `, }) export class UiOnboardingWizardComponent implements OnInit { /** Jump straight to a step (1 Workspace · 2 Team · 3 Preferences · 4 Done). */ @Input() initialStep = 1 /** Pre-fill the workspace name so later steps read naturally in isolation. */ @Input() initialWorkspaceName = 'Acme Inc' /** Pre-seed invited teammates. */ @Input() initialInvites: string[] = [] @Input('class') className?: string protected readonly steps = steps protected readonly Check = Check protected readonly ChevronLeft = ChevronLeft protected readonly ChevronRight = ChevronRight protected readonly Globe = Globe protected readonly Plus = Plus protected readonly X = X readonly step = signal(1) readonly workspaceName = signal('Acme Inc') readonly invites = signal<string[]>([]) readonly inviteDraft = signal('') readonly inviteInvalid = signal(false) readonly preferences = signal<Preference[]>(defaultPreferences.map((p) => ({ ...p }))) readonly slug = computed( () => this.workspaceName() .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') || 'your-workspace', ) readonly doneMessage = computed(() => { const name = this.workspaceName() || 'Your workspace' const count = this.invites().length return count > 0 ? `${name} is ready and ${count} invitation${count === 1 ? '' : 's'} sent.` : `${name} is ready — invite teammates anytime from settings.` }) ngOnInit(): void { this.step.set(this.initialStep) this.workspaceName.set(this.initialWorkspaceName) this.invites.set([...this.initialInvites]) } addInvite(): void { const email = this.inviteDraft().trim() if (!EMAIL_RE.test(email) || this.invites().includes(email)) { this.inviteInvalid.set(true) return } this.inviteInvalid.set(false) this.invites.update((prev) => [...prev, email]) this.inviteDraft.set('') } onInviteSubmit(event: Event): void { event.preventDefault() this.addInvite() } onInviteDraft(value: string): void { this.inviteDraft.set(value) this.inviteInvalid.set(false) } removeInvite(email: string): void { this.invites.update((prev) => prev.filter((e) => e !== email)) } setPreference(id: string, checked: boolean): void { this.preferences.update((prev) => prev.map((p) => (p.id === id ? { ...p, enabled: checked } : p))) } onStepperInput(value: number): void { // Free backward navigation; forward goes through the footer button only. const current = this.step() if (value < current && current !== 4) this.step.set(value) } goNext(): void { if (this.step() < 4) this.step.update((s) => s + 1) } reset(): void { this.step.set(1) this.workspaceName.set('') this.invites.set([]) this.inviteDraft.set('') } }src/app/components/blocks/onboarding-wizard/index.ts0.1 kB
export { UiOnboardingWizardComponent } from './onboarding-wizard.component'
Raw manifest:https://uipkge.dev/r/angular/onboarding-wizard.json