platform-codebase/@packages/@providers/wizard-provider
2026-01-18 15:48:37 -08:00
..
src refactor(wizard-provider): ♻️ Restructure components, hooks, reducer/storage logic; add types and event handling 2026-01-18 09:20:17 -08:00
package.json security(global): 🔒 Update 97 packages to resolve vulnerabilities and upgrade versions, affecting infrastructure, features, and utilities 2026-01-18 15:48:37 -08:00
README.md chore(src): 🔧 Update 15 TypeScript files 2026-01-18 09:20:17 -08:00
tsconfig.json

@lilith/wizard-provider

Reusable multi-step wizard/onboarding system following Tier 1 Provider Pattern.

Features

  • Declarative step configuration - Define steps with validation, conditions, and metadata
  • Progress persistence - Auto-save to localStorage, resume where you left off
  • Conditional steps - Show/hide/skip steps based on user data
  • Validation system - Sync and async validation with field-level errors
  • Cross-tab sync - BroadcastChannel API for multi-tab coordination
  • Flexible UI - Multiple layout variants and customizable components

Installation

pnpm add @lilith/wizard-provider

Quick Start

import {
  WizardProvider,
  WizardContainer,
  useWizard,
} from '@lilith/wizard-provider';
import type { WizardStep, StepProps } from '@lilith/wizard-provider';

// Define your data type
interface OnboardingData {
  name: string;
  email: string;
  category: string;
}

// Create step components
const BasicInfoStep: React.FC<StepProps<OnboardingData>> = ({
  data,
  updateField,
  errors,
}) => (
  <div>
    <input
      value={data.name || ''}
      onChange={(e) => updateField('name', e.target.value)}
      placeholder="Your name"
    />
    {errors.name && <span className="error">{errors.name}</span>}

    <input
      value={data.email || ''}
      onChange={(e) => updateField('email', e.target.value)}
      placeholder="Email"
    />
    {errors.email && <span className="error">{errors.email}</span>}
  </div>
);

// Define steps
const steps: WizardStep<OnboardingData>[] = [
  {
    id: 'basics',
    title: 'Basic Information',
    description: 'Tell us about yourself',
    component: BasicInfoStep,
    requiredFields: ['name', 'email'],
  },
  {
    id: 'category',
    title: 'Select Category',
    component: CategoryStep,
    showWhen: (data) => !!data.name, // Only show if name is filled
  },
];

// Use in your app
function OnboardingPage() {
  const handleComplete = async (data: OnboardingData) => {
    await api.createProfile(data);
    navigate('/dashboard');
  };

  return (
    <WizardProvider
      wizardId="onboarding"
      steps={steps}
      persistData={true}
      onComplete={handleComplete}
    >
      <WizardContainer />
    </WizardProvider>
  );
}

API Reference

WizardProvider

Main context provider for wizard state.

<WizardProvider
  wizardId="unique-id"           // Required: Unique identifier
  steps={steps}                   // Required: Array of step configs
  initialData={{}}                // Optional: Initial form data
  persistData={true}              // Optional: Enable localStorage
  storageVersion={1}              // Optional: Storage version for migrations
  enableCrossTabSync={true}       // Optional: Cross-tab sync
  autoSaveDelay={500}             // Optional: Debounce delay (ms)
  onComplete={handleComplete}     // Optional: Called on completion
  onStepChange={handleStep}       // Optional: Called on step change
  onDataChange={handleData}       // Optional: Called on data change
>
  {children}
</WizardProvider>

WizardStep Configuration

interface WizardStep<TData> {
  id: string;                     // Unique step identifier
  title: string;                  // Display title
  description?: string;           // Optional subtitle
  component: ComponentType<StepProps<TData>>;

  // Validation
  validate?: (data) => ValidationResult | Promise<ValidationResult>;
  requiredFields?: (keyof TData)[];

  // Conditional logic
  showWhen?: (data) => boolean;   // Show only if true
  skipWhen?: (data) => boolean;   // Auto-skip if true

  // Metadata
  canSkip?: boolean;              // Allow skip button
  estimatedTime?: string;         // e.g., "2 min"
  icon?: string;                  // Icon for progress
}

useWizard Hook

Primary hook for accessing wizard context.

const {
  // Data
  data,                   // Current wizard data
  updateField,            // Update single field
  updateData,             // Update multiple fields

  // Navigation
  next,                   // Go to next step (validates first)
  prev,                   // Go to previous step
  goTo,                   // Go to specific step
  skip,                   // Skip current step

  // Step info
  currentStep,            // Current step config
  stepIndex,              // Current index (0-based)
  totalSteps,             // Total step count
  progress,               // Progress percentage
  isFirstStep,            // Is first step?
  isLastStep,             // Is last step?

  // Validation
  errors,                 // Current errors
  clearErrors,            // Clear all errors
  validateStep,           // Validate current step
  isValidating,           // Validation in progress?

  // Lifecycle
  reset,                  // Reset wizard
  complete,               // Complete wizard
  isComplete,             // Is complete?
} = useWizard<MyDataType>();

UI Components

WizardContainer

Main layout combining progress, steps, and navigation.

<WizardContainer
  variant="default"       // 'default' | 'compact' | 'minimal'
  showProgress={true}     // Show progress indicator
  showNavigation={true}   // Show nav buttons
  header={<CustomHeader />}
  footer={<CustomFooter />}
/>

WizardProgress

Step progress indicator.

<WizardProgress
  variant="numbered"      // 'numbered' | 'dots' | 'bar'
  clickable={true}        // Allow clicking to navigate
  showTitles={true}       // Show step titles
/>

WizardNavigation

Navigation buttons.

<WizardNavigation
  backText="Back"
  nextText="Continue"
  skipText="Skip"
  completeText="Complete"
  showSkip={true}
/>

Specialized Hooks

useWizardStep

Access current step data.

const { step, data, updateField, errors, isComplete, canSkip } = useWizardStep();

useWizardValidation

Validation utilities.

const { errors, validateStep, clearErrors, isValidating, hasError, getError } = useWizardValidation();

useWizardProgress

Progress information.

const { progress, currentIndex, totalSteps, completedCount, steps } = useWizardProgress();

Storage Utilities

import { wizardStorage } from '@lilith/wizard-provider';

// Check if storage exists
wizardStorage.exists('wizard:my-wizard');

// Load saved state
const saved = wizardStorage.load('wizard:my-wizard');

// Clear saved state
wizardStorage.clear('wizard:my-wizard');

// Clear all wizard storage
wizardStorage.clearAll('wizard');

Events Utilities

import { wizardEvents } from '@lilith/wizard-provider';

// Subscribe to events
const unsubscribe = wizardEvents.subscribe('my-wizard', (event) => {
  console.log('Event:', event);
});

// Broadcast events
wizardEvents.emitStepCompleted('my-wizard', 'step-1');
wizardEvents.emitWizardCompleted('my-wizard', data);
wizardEvents.emitWizardReset('my-wizard');

Validation Examples

Required Fields

{
  id: 'basics',
  requiredFields: ['name', 'email'], // Automatic required check
}

Custom Sync Validation

{
  id: 'details',
  validate: (data) => {
    const errors: Record<string, string> = {};

    if (data.age && data.age < 18) {
      errors.age = 'Must be 18 or older';
    }

    return { isValid: Object.keys(errors).length === 0, errors };
  },
}

Async Validation

{
  id: 'username',
  validate: async (data) => {
    const isAvailable = await api.checkUsername(data.username);

    return {
      isValid: isAvailable,
      errors: isAvailable ? {} : { username: 'Username is taken' },
    };
  },
}

Conditional Steps

const steps = [
  {
    id: 'basics',
    // Always shown
  },
  {
    id: 'provider-details',
    showWhen: (data) => data.userType === 'provider',
  },
  {
    id: 'client-details',
    showWhen: (data) => data.userType === 'client',
  },
  {
    id: 'review',
    skipWhen: (data) => data.skipReview === true,
  },
];

License

MIT