Skip to main content
Glama
wizard-page.ts10 kB
/** * Wizard Page Component * * Specialized page component for multi-step wizards. * Provides consistent header with step progress and footer with navigation hints. * * @package WP_Navigator_Pro * @since 2.5.0 * * @example * import { createWizardPage } from './wizard-page.js'; * * const wizardPage = createWizardPage({ * title: 'WP Navigator Setup', * currentStep: 2, * totalSteps: 5, * }); * * wizardPage.render('Configure your WordPress connection...'); */ import { createPage, type PageTUI, type PageOptions, rightAlign } from './page.js'; import { getTerminalSize, supportsPageTUI, type TerminalSize } from './terminal.js'; import { colorize, supportsColor, symbols } from './components.js'; // ============================================================================= // Types // ============================================================================= /** * Options for creating a wizard page */ export interface WizardPageOptions { /** Wizard title (shown in header) */ title: string; /** Current step number (1-based) */ currentStep: number; /** Total number of steps */ totalSteps: number; /** Show back navigation hint (default: true for step > 1) */ showBackHint?: boolean; /** Show help hint (default: true) */ showHelpHint?: boolean; /** Show quit hint (default: true) */ showQuitHint?: boolean; /** Custom footer text (overrides navigation hints) */ customFooter?: string; /** Output stream (default: process.stderr) */ output?: NodeJS.WriteStream; } /** * Wizard page instance interface */ export interface WizardPage { /** Clear screen and prepare for new step */ clear(): void; /** Render content for current step */ render(content: string): void; /** Render complete step (clears, shows header/content/footer) */ renderStep(content: string): void; /** Update step progress (without clearing) */ updateStep(currentStep: number, totalSteps?: number): void; /** Show navigation hints in footer */ showNavigationHints(): void; /** Show help overlay with contextual help text */ showHelp(helpText: string): void; /** Check if page-based rendering is supported */ isSupported(): boolean; /** Get current terminal size */ getSize(): TerminalSize; /** Get the underlying page instance */ getPage(): PageTUI; } /** * Internal wizard state */ interface WizardState { title: string; currentStep: number; totalSteps: number; showBackHint: boolean; showHelpHint: boolean; showQuitHint: boolean; customFooter?: string; } // ============================================================================= // Wizard Page Factory // ============================================================================= /** * Create a new wizard page instance * * @param options - Wizard page configuration * @returns WizardPage instance */ export function createWizardPage(options: WizardPageOptions): WizardPage { const { title, currentStep, totalSteps, showBackHint = currentStep > 1, showHelpHint = true, showQuitHint = true, customFooter, output = process.stderr, } = options; // Initialize state const state: WizardState = { title, currentStep, totalSteps, showBackHint, showHelpHint, showQuitHint, customFooter, }; // Create underlying page const page = createPage({ reserveHeaderLines: 3, reserveFooterLines: 2, output, }); /** * Generate header with title and step progress */ function generateHeader(): string { const { width } = getTerminalSize(); const stepText = `Step ${state.currentStep}/${state.totalSteps}`; if (supportsPageTUI()) { // Full-width header with title left, progress right const padding = width - state.title.length - stepText.length - 2; if (padding > 0) { return `${state.title}${' '.repeat(padding)}${stepText}`; } // Narrow terminal: stack vertically return `${state.title}\n${stepText}`; } // Non-TTY: simple header return `${state.title} — ${stepText}`; } /** * Generate footer with navigation hints */ function generateFooter(): string { if (state.customFooter) { return state.customFooter; } const hints: string[] = []; if (state.showBackHint && state.currentStep > 1) { hints.push('[B]ack'); } if (state.showHelpHint) { hints.push('[H]elp'); } if (state.showQuitHint) { hints.push('[Q]uit'); } if (hints.length === 0) { return 'Press Enter to continue'; } return hints.join(' '); } /** * Generate step progress indicator (visual) */ function generateProgressIndicator(): string { const { currentStep, totalSteps } = state; const filled = currentStep; const empty = totalSteps - currentStep; // Use circles for progress: ● for completed/current, ○ for remaining const indicator = '●'.repeat(filled) + '○'.repeat(empty); if (supportsColor()) { return colorize(indicator, 'cyan'); } return indicator; } /** * Clear screen */ function clear(): void { page.clear(); } /** * Render content */ function render(content: string): void { page.render(content); } /** * Render complete step (clear + header + content + footer) */ function renderStep(content: string): void { page.renderPage({ header: generateHeader(), content, footer: generateFooter(), }); } /** * Update step progress */ function updateStep(newCurrentStep: number, newTotalSteps?: number): void { state.currentStep = newCurrentStep; if (newTotalSteps !== undefined) { state.totalSteps = newTotalSteps; } // Update back hint visibility based on step state.showBackHint = newCurrentStep > 1; } /** * Show navigation hints in footer */ function showNavigationHints(): void { page.renderFooter(generateFooter()); } /** * Show help overlay with contextual help text */ function showHelp(helpText: string): void { const { width } = getTerminalSize(); const boxWidth = Math.min(width - 4, 70); // Create help box content const lines = helpText.split('\n'); const paddedLines = lines.map((line) => { const truncated = line.slice(0, boxWidth - 6); return truncated; }); // Build box manually for better control const topLine = '┌─ Help ' + '─'.repeat(Math.max(0, boxWidth - 9)) + '┐'; const bottomLine = '└' + '─'.repeat(boxWidth - 2) + '┘'; const boxedLines = paddedLines.map((line) => { const padded = line.padEnd(boxWidth - 4); return '│ ' + padded + ' │'; }); const helpBox = [topLine, ...boxedLines, bottomLine].join('\n'); // Output help if (supportsPageTUI()) { page.clear(); page.renderHeader(generateHeader()); } output.write('\n'); output.write(helpBox); output.write('\n\n'); if (supportsColor()) { output.write(colorize('Press any key to continue...', 'dim')); } else { output.write('Press any key to continue...'); } output.write('\n'); } /** * Check if page-based rendering is supported */ function isSupported(): boolean { return page.isSupported(); } /** * Get current terminal size */ function getSize(): TerminalSize { return page.getSize(); } /** * Get underlying page instance */ function getPage(): PageTUI { return page; } return { clear, render, renderStep, updateStep, showNavigationHints, showHelp, isSupported, getSize, getPage, }; } // ============================================================================= // Utility Functions // ============================================================================= /** * Format a wizard step title with optional icon * * @param stepNumber - Step number * @param title - Step title * @param options - Formatting options * @returns Formatted step title */ export function formatStepTitle( stepNumber: number, title: string, options: { showIcon?: boolean; icon?: string } = {} ): string { const { showIcon = true, icon = symbols.arrow } = options; if (showIcon) { return `${icon} Step ${stepNumber}: ${title}`; } return `Step ${stepNumber}: ${title}`; } /** * Format step completion message * * @param stepNumber - Completed step number * @param message - Completion message * @returns Formatted completion message */ export function formatStepComplete(stepNumber: number, message?: string): string { const checkmark = supportsColor() ? colorize(symbols.success, 'green') : symbols.success; const text = message ?? `Step ${stepNumber} complete`; return `${checkmark} ${text}`; } /** * Format step error message * * @param stepNumber - Failed step number * @param error - Error message * @returns Formatted error message */ export function formatStepError(stepNumber: number, error: string): string { const cross = supportsColor() ? colorize(symbols.error, 'red') : symbols.error; return `${cross} Step ${stepNumber} failed: ${error}`; } /** * Create a progress bar for wizard steps * * @param currentStep - Current step (1-based) * @param totalSteps - Total number of steps * @param options - Formatting options * @returns Progress bar string */ export function createStepProgressBar( currentStep: number, totalSteps: number, options: { width?: number; showPercentage?: boolean } = {} ): string { const { width = 20, showPercentage = false } = options; const percent = Math.round((currentStep / totalSteps) * 100); const filled = Math.round((currentStep / totalSteps) * width); const empty = width - filled; let bar = '█'.repeat(filled) + '░'.repeat(empty); if (supportsColor()) { bar = colorize(bar, 'cyan'); } if (showPercentage) { return `${bar} ${percent}%`; } return `${bar} ${currentStep}/${totalSteps}`; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/littlebearapps/wp-navigator-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server