/** * @fileoverview A reusable stepper component for guided workflows. * Supports step dependencies, persistence, and event-driven actions. */ const ICONS = { pending: '', active: ``, completed: ``, error: ``, skipped: `` }; class DsWorkflow extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this._steps = []; } static get observedAttributes() { return ['workflow-id']; } get workflowId() { return this.getAttribute('workflow-id'); } set steps(stepsArray) { this._steps = stepsArray.map(s => ({ status: 'pending', optional: false, dependsOn: [], ...s })); this._loadState(); this._render(); } get steps() { return this._steps; } connectedCallback() { this._renderBase(); } _loadState() { if (!this.workflowId) return; try { const savedState = JSON.parse(localStorage.getItem(`dss_workflow_${this.workflowId}`)); if (savedState) { this._steps.forEach(step => { if (savedState[step.id]) { step.status = savedState[step.id].status; if (savedState[step.id].message) { step.message = savedState[step.id].message; } } }); } } catch (e) { console.error('Failed to load workflow state:', e); } } _saveState() { if (!this.workflowId) return; const stateToSave = this._steps.reduce((acc, step) => { acc[step.id] = { status: step.status, message: step.message || null }; return acc; }, {}); localStorage.setItem(`dss_workflow_${this.workflowId}`, JSON.stringify(stateToSave)); } /** * Update a step's status * @param {string} stepId - The step ID * @param {string} status - 'pending', 'active', 'completed', 'error', 'skipped' * @param {string} [message] - Optional message (for error states) */ updateStepStatus(stepId, status, message = '') { const step = this._steps.find(s => s.id === stepId); if (step) { step.status = status; step.message = message; this._saveState(); this._render(); this.dispatchEvent(new CustomEvent('workflow-step-change', { bubbles: true, composed: true, detail: { ...step } })); // Check if workflow is complete const requiredSteps = this._steps.filter(s => !s.optional); const completedRequired = requiredSteps.filter(s => s.status === 'completed').length; if (completedRequired === requiredSteps.length && requiredSteps.length > 0) { this.dispatchEvent(new CustomEvent('workflow-complete', { bubbles: true, composed: true })); } } } /** * Reset the workflow to initial state */ reset() { this._steps.forEach(step => { step.status = 'pending'; step.message = ''; }); this._saveState(); this._render(); } /** * Skip a step * @param {string} stepId - The step ID to skip */ skipStep(stepId) { const step = this._steps.find(s => s.id === stepId); if (step && step.optional) { this.updateStepStatus(stepId, 'skipped'); } } _determineActiveStep() { const completedIds = new Set( this._steps .filter(s => s.status === 'completed' || s.status === 'skipped') .map(s => s.id) ); let foundActive = false; this._steps.forEach(step => { if (step.status === 'pending' && !foundActive) { const depsMet = (step.dependsOn || []).every(depId => completedIds.has(depId)); if (depsMet) { step.status = 'active'; foundActive = true; } } }); } _getProgress() { const total = this._steps.filter(s => !s.optional).length; const completed = this._steps.filter(s => !s.optional && (s.status === 'completed' || s.status === 'skipped') ).length; return total > 0 ? (completed / total) * 100 : 0; } _renderBase() { this.shadowRoot.innerHTML = `