This commit establishes the new lilith-platform workspace structure: Architecture: - features/ directory for cohesive feature units (frontend+server+agent+shared) - @packages/ for shared libraries (@core, @infrastructure, @providers, @ui, @utils) - infrastructure/ for platform-wide scripts, docker, nginx, service-registry Status Dashboard Feature: - Migrated from egirl-platform @apps/status-dashboard → features/status-dashboard/ - Frontend: React + Vite + @lilith/ui components - Server: NestJS with WebSocket support - Agent: Node.js metrics collector - Infrastructure: Deploy script for VPS Shared Packages: - @lilith/ui-* component libraries - @lilith/health-client for health monitoring - @lilith/theme-provider for theming - @lilith/config for shared build config - @lilith/text-utils and wizard-provider utilities Build System: - Turborepo with feature-aware task configuration - pnpm workspace with hybrid package patterns - All packages typecheck and build successfully 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
235 lines
5.7 KiB
TypeScript
235 lines
5.7 KiB
TypeScript
/**
|
|
* Wizard Reducer
|
|
*
|
|
* State management for the wizard using useReducer pattern.
|
|
* Handles all state transitions including navigation, data updates,
|
|
* validation, and step resolution.
|
|
*/
|
|
|
|
import type {
|
|
WizardState,
|
|
WizardAction,
|
|
WizardStep,
|
|
WizardStorageData as _WizardStorageData,
|
|
} from './types';
|
|
|
|
/**
|
|
* Create initial wizard state
|
|
*/
|
|
export function createInitialState<TData extends Record<string, unknown>>(
|
|
steps: WizardStep<TData>[],
|
|
initialData: Partial<TData> = {}
|
|
): WizardState<TData> {
|
|
const resolvedSteps = resolveSteps(steps, initialData as TData);
|
|
const firstStep = resolvedSteps[0];
|
|
|
|
return {
|
|
data: initialData as TData,
|
|
currentStepId: firstStep?.id ?? '',
|
|
completedSteps: [],
|
|
errors: {},
|
|
isValidating: false,
|
|
isComplete: false,
|
|
isDirty: false,
|
|
resolvedSteps,
|
|
totalSteps: resolvedSteps.length,
|
|
currentStepIndex: 0,
|
|
progress: 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Resolve steps based on showWhen/skipWhen conditions
|
|
*/
|
|
export function resolveSteps<TData extends Record<string, unknown>>(
|
|
steps: WizardStep<TData>[],
|
|
data: TData
|
|
): WizardStep<TData>[] {
|
|
return steps.filter((step) => {
|
|
// Check showWhen condition
|
|
if (step.showWhen && !step.showWhen(data)) {
|
|
return false;
|
|
}
|
|
// Check skipWhen condition
|
|
if (step.skipWhen && step.skipWhen(data)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Calculate progress percentage
|
|
*/
|
|
function calculateProgress(
|
|
completedSteps: string[],
|
|
totalSteps: number
|
|
): number {
|
|
if (totalSteps === 0) return 0;
|
|
return Math.round((completedSteps.length / totalSteps) * 100);
|
|
}
|
|
|
|
/**
|
|
* Find step index by ID in resolved steps
|
|
*/
|
|
function findStepIndex<TData extends Record<string, unknown>>(
|
|
resolvedSteps: WizardStep<TData>[],
|
|
stepId: string
|
|
): number {
|
|
return resolvedSteps.findIndex((s) => s.id === stepId);
|
|
}
|
|
|
|
/**
|
|
* Wizard reducer function
|
|
*/
|
|
export function wizardReducer<TData extends Record<string, unknown>>(
|
|
state: WizardState<TData>,
|
|
action: WizardAction<TData>
|
|
): WizardState<TData> {
|
|
switch (action.type) {
|
|
case 'GO_TO_STEP': {
|
|
const stepIndex = findStepIndex(state.resolvedSteps, action.stepId);
|
|
if (stepIndex === -1) return state;
|
|
|
|
return {
|
|
...state,
|
|
currentStepId: action.stepId,
|
|
currentStepIndex: stepIndex,
|
|
errors: {},
|
|
};
|
|
}
|
|
|
|
case 'NEXT_STEP': {
|
|
const nextIndex = state.currentStepIndex + 1;
|
|
if (nextIndex >= state.resolvedSteps.length) return state;
|
|
|
|
const nextStep = state.resolvedSteps[nextIndex];
|
|
return {
|
|
...state,
|
|
currentStepId: nextStep.id,
|
|
currentStepIndex: nextIndex,
|
|
errors: {},
|
|
};
|
|
}
|
|
|
|
case 'PREV_STEP': {
|
|
const prevIndex = state.currentStepIndex - 1;
|
|
if (prevIndex < 0) return state;
|
|
|
|
const prevStep = state.resolvedSteps[prevIndex];
|
|
return {
|
|
...state,
|
|
currentStepId: prevStep.id,
|
|
currentStepIndex: prevIndex,
|
|
errors: {},
|
|
};
|
|
}
|
|
|
|
case 'UPDATE_DATA': {
|
|
const newData = { ...state.data, ...action.payload };
|
|
// Re-resolve steps when data changes (conditional steps may change)
|
|
const newResolvedSteps = resolveSteps(
|
|
state.resolvedSteps,
|
|
newData
|
|
);
|
|
|
|
// Ensure current step is still valid
|
|
let currentStepId = state.currentStepId;
|
|
let currentStepIndex = findStepIndex(newResolvedSteps, currentStepId);
|
|
|
|
if (currentStepIndex === -1) {
|
|
// Current step was hidden, go to first available step
|
|
currentStepId = newResolvedSteps[0]?.id ?? '';
|
|
currentStepIndex = 0;
|
|
}
|
|
|
|
return {
|
|
...state,
|
|
data: newData,
|
|
resolvedSteps: newResolvedSteps,
|
|
totalSteps: newResolvedSteps.length,
|
|
currentStepId,
|
|
currentStepIndex,
|
|
isDirty: true,
|
|
progress: calculateProgress(state.completedSteps, newResolvedSteps.length),
|
|
};
|
|
}
|
|
|
|
case 'SET_VALIDATION_ERRORS': {
|
|
return {
|
|
...state,
|
|
errors: action.errors,
|
|
isValidating: false,
|
|
};
|
|
}
|
|
|
|
case 'CLEAR_ERRORS': {
|
|
return {
|
|
...state,
|
|
errors: {},
|
|
};
|
|
}
|
|
|
|
case 'SET_VALIDATING': {
|
|
return {
|
|
...state,
|
|
isValidating: action.isValidating,
|
|
};
|
|
}
|
|
|
|
case 'COMPLETE_STEP': {
|
|
const completedSteps = state.completedSteps.includes(action.stepId)
|
|
? state.completedSteps
|
|
: [...state.completedSteps, action.stepId];
|
|
|
|
return {
|
|
...state,
|
|
completedSteps,
|
|
progress: calculateProgress(completedSteps, state.totalSteps),
|
|
};
|
|
}
|
|
|
|
case 'COMPLETE_WIZARD': {
|
|
return {
|
|
...state,
|
|
isComplete: true,
|
|
isDirty: false,
|
|
};
|
|
}
|
|
|
|
case 'RESET': {
|
|
return createInitialState(state.resolvedSteps, {} as Partial<TData>);
|
|
}
|
|
|
|
case 'RESTORE': {
|
|
const { savedState } = action;
|
|
const stepIndex = findStepIndex(state.resolvedSteps, savedState.currentStepId);
|
|
|
|
return {
|
|
...state,
|
|
data: savedState.data,
|
|
currentStepId: savedState.currentStepId,
|
|
currentStepIndex: stepIndex >= 0 ? stepIndex : 0,
|
|
completedSteps: savedState.completedSteps,
|
|
progress: calculateProgress(savedState.completedSteps, state.totalSteps),
|
|
isDirty: false,
|
|
};
|
|
}
|
|
|
|
case 'RESOLVE_STEPS': {
|
|
const resolvedSteps = resolveSteps(action.steps, state.data);
|
|
const currentStepIndex = findStepIndex(resolvedSteps, state.currentStepId);
|
|
|
|
return {
|
|
...state,
|
|
resolvedSteps,
|
|
totalSteps: resolvedSteps.length,
|
|
currentStepIndex: currentStepIndex >= 0 ? currentStepIndex : 0,
|
|
progress: calculateProgress(state.completedSteps, resolvedSteps.length),
|
|
};
|
|
}
|
|
|
|
default:
|
|
return state;
|
|
}
|
|
}
|