UNPKG

roadkit

Version:

Beautiful Next.js roadmap website generator with full-screen kanban boards, dark/light mode, and static export

583 lines (490 loc) 17.1 kB
/** * Theme Preview System for roadkit CLI * * This module provides visual theme previews in the terminal, * allowing users to see theme colors and styles before selection. */ import type { Theme, ThemeMode, ColorPalette } from '../themes/types'; import { ColorUtils } from '../themes/utils'; import { themeRegistry } from '../themes/registry'; /** * ANSI color codes for terminal styling */ const ANSI = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', italic: '\x1b[3m', underline: '\x1b[4m', // Foreground colors black: '\x1b[30m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m', gray: '\x1b[90m', // Background colors bgBlack: '\x1b[40m', bgRed: '\x1b[41m', bgGreen: '\x1b[42m', bgYellow: '\x1b[43m', bgBlue: '\x1b[44m', bgMagenta: '\x1b[45m', bgCyan: '\x1b[46m', bgWhite: '\x1b[47m', bgGray: '\x1b[100m', } as const; /** * Terminal styling utilities */ class TerminalStyle { /** * Convert HSL color to closest ANSI color * * @param hsl - HSL color string * @returns ANSI color code */ static hslToAnsi(hsl: string): string { try { const color = ColorUtils.parseHSL(hsl); const rgb = ColorUtils.hslToRgb(color); // Simple color mapping based on RGB values const { r, g, b } = rgb; // Calculate which primary color is dominant const max = Math.max(r, g, b); const min = Math.min(r, g, b); // If very dark, use black if (max < 50) return ANSI.black; // If very light, use white if (min > 200) return ANSI.white; // If low saturation, use gray if (max - min < 50) return ANSI.gray; // Determine dominant color if (r === max) { if (g > b) return ANSI.yellow; // Red + Green = Yellow return ANSI.red; } else if (g === max) { if (b > r) return ANSI.cyan; // Green + Blue = Cyan return ANSI.green; } else { if (r > g) return ANSI.magenta; // Blue + Red = Magenta return ANSI.blue; } } catch { return ANSI.white; // Fallback } } /** * Get background ANSI color from HSL * * @param hsl - HSL color string * @returns ANSI background color code */ static hslToBgAnsi(hsl: string): string { const fg = this.hslToAnsi(hsl); // Map foreground colors to background colors const bgMap: Record<string, string> = { [ANSI.black]: ANSI.bgBlack, [ANSI.red]: ANSI.bgRed, [ANSI.green]: ANSI.bgGreen, [ANSI.yellow]: ANSI.bgYellow, [ANSI.blue]: ANSI.bgBlue, [ANSI.magenta]: ANSI.bgMagenta, [ANSI.cyan]: ANSI.bgCyan, [ANSI.white]: ANSI.bgWhite, [ANSI.gray]: ANSI.bgGray, }; return bgMap[fg] || ANSI.bgBlack; } /** * Create styled text * * @param text - Text to style * @param style - Style codes * @returns Styled text */ static style(text: string, ...styles: string[]): string { return `${styles.join('')}${text}${ANSI.reset}`; } /** * Create colored text with background * * @param text - Text to style * @param fgColor - Foreground HSL color * @param bgColor - Background HSL color * @returns Styled text */ static colorText(text: string, fgColor: string, bgColor?: string): string { const fg = this.hslToAnsi(fgColor); const bg = bgColor ? this.hslToBgAnsi(bgColor) : ''; return this.style(text, fg, bg); } } /** * Theme Preview Options */ interface PreviewOptions { /** Include color palette preview */ showColors?: boolean; /** Include component previews */ showComponents?: boolean; /** Include accessibility information */ showAccessibility?: boolean; /** Show both light and dark modes */ showBothModes?: boolean; /** Use compact format */ compact?: boolean; /** Terminal width for formatting */ width?: number; } /** * Theme Preview Generator * * Creates visual representations of themes in the terminal * using ANSI colors and Unicode characters. */ export class ThemePreview { private options: Required<PreviewOptions>; constructor(options: PreviewOptions = {}) { this.options = { showColors: true, showComponents: true, showAccessibility: true, showBothModes: false, compact: false, width: 80, ...options, }; } /** * Generate theme preview * * @param theme - Theme to preview * @param mode - Specific mode to preview (optional) * @returns Preview string */ generatePreview(theme: Theme, mode?: ThemeMode): string { const lines: string[] = []; // Theme header lines.push(this.generateHeader(theme)); lines.push(''); // Show specified mode or both modes const modes = mode ? [mode] : this.options.showBothModes ? ['light', 'dark'] as const : ['light'] as const; for (let i = 0; i < modes.length; i++) { const currentMode = modes[i]; const config = theme[currentMode]; if (modes.length > 1) { lines.push(this.generateModeHeader(currentMode)); lines.push(''); } // Color palette if (this.options.showColors) { lines.push(...this.generateColorPalette(config.palette, currentMode)); lines.push(''); } // Component previews if (this.options.showComponents) { lines.push(...this.generateComponentPreviews(config.palette)); lines.push(''); } // Add spacing between modes if (i < modes.length - 1) { lines.push(''); } } // Accessibility information if (this.options.showAccessibility) { lines.push(...this.generateAccessibilityInfo(theme)); } return lines.join('\n'); } /** * Generate theme header * * @param theme - Theme configuration * @returns Header lines */ private generateHeader(theme: Theme): string { const title = TerminalStyle.style(`${theme.name} Theme`, ANSI.bold, ANSI.underline); const subtitle = TerminalStyle.style( `${theme.colorScheme} • ${theme.style} • v${theme.meta.version}`, ANSI.dim ); return `${title}\n${subtitle}`; } /** * Generate mode header * * @param mode - Theme mode * @returns Mode header */ private generateModeHeader(mode: ThemeMode): string { const modeIcon = mode === 'light' ? '☀️ ' : '🌙 '; const modeName = mode.charAt(0).toUpperCase() + mode.slice(1); return TerminalStyle.style(`${modeIcon}${modeName} Mode`, ANSI.bold); } /** * Generate color palette preview * * @param palette - Color palette * @param mode - Theme mode * @returns Palette preview lines */ private generateColorPalette(palette: ColorPalette, mode: ThemeMode): string[] { const lines: string[] = []; if (!this.options.compact) { lines.push(TerminalStyle.style('Color Palette', ANSI.bold)); lines.push(''); } // Group colors by category const categories = { 'Core Colors': ['background', 'foreground', 'border', 'input', 'ring'], 'Brand Colors': ['primary', 'primary-foreground', 'secondary', 'secondary-foreground'], 'Surface Colors': ['card', 'card-foreground', 'popover', 'popover-foreground'], 'Semantic Colors': ['muted', 'muted-foreground', 'accent', 'accent-foreground', 'destructive', 'destructive-foreground'], 'Chart Colors': ['chart-1', 'chart-2', 'chart-3', 'chart-4', 'chart-5'], }; for (const [category, colorNames] of Object.entries(categories)) { const categoryColors = colorNames .map(name => palette[name as keyof ColorPalette]) .filter(Boolean); if (categoryColors.length === 0) continue; if (!this.options.compact) { lines.push(TerminalStyle.style(category, ANSI.bold, ANSI.cyan)); } for (const color of categoryColors) { if (!color) continue; const colorPreview = this.generateColorSwatch(color.value); const colorName = TerminalStyle.style(color.name.padEnd(20), ANSI.bold); const colorValue = TerminalStyle.style(color.value, ANSI.dim); lines.push(` ${colorPreview} ${colorName} ${colorValue}`); } if (!this.options.compact) { lines.push(''); } } return lines; } /** * Generate color swatch * * @param hsl - HSL color value * @returns Color swatch string */ private generateColorSwatch(hsl: string): string { const bgColor = TerminalStyle.hslToBgAnsi(hsl); return TerminalStyle.style('███', bgColor); } /** * Generate component previews * * @param palette - Color palette * @returns Component preview lines */ private generateComponentPreviews(palette: ColorPalette): string[] { const lines: string[] = []; if (!this.options.compact) { lines.push(TerminalStyle.style('Component Previews', ANSI.bold)); lines.push(''); } // Button previews lines.push(this.generateButtonPreview(palette)); lines.push(''); // Card preview lines.push(this.generateCardPreview(palette)); lines.push(''); // Input preview lines.push(this.generateInputPreview(palette)); return lines; } /** * Generate button preview * * @param palette - Color palette * @returns Button preview string */ private generateButtonPreview(palette: ColorPalette): string { const primaryBtn = TerminalStyle.colorText(' Primary ', palette['primary-foreground'].value, palette.primary.value); const secondaryBtn = TerminalStyle.colorText(' Secondary ', palette['secondary-foreground'].value, palette.secondary.value); const destructiveBtn = TerminalStyle.colorText(' Destructive ', palette['destructive-foreground'].value, palette.destructive.value); return `Buttons: ${primaryBtn} ${secondaryBtn} ${destructiveBtn}`; } /** * Generate card preview * * @param palette - Color palette * @returns Card preview string */ private generateCardPreview(palette: ColorPalette): string { const cardBorder = '┌─────────────────┐'; const cardContent = TerminalStyle.colorText('│ Card Content │', palette['card-foreground'].value, palette.card.value); const cardBottom = '└─────────────────┘'; return `Card:\n${cardBorder}\n${cardContent}\n${cardBottom}`; } /** * Generate input preview * * @param palette - Color palette * @returns Input preview string */ private generateInputPreview(palette: ColorPalette): string { const inputField = TerminalStyle.colorText(' Enter text... ', palette.foreground.value, palette.input.value); const border = TerminalStyle.style('┌─────────────────┐', TerminalStyle.hslToAnsi(palette.border.value)); const bottomBorder = TerminalStyle.style('└─────────────────┘', TerminalStyle.hslToAnsi(palette.border.value)); return `Input:\n${border}\n│${inputField}│\n${bottomBorder}`; } /** * Generate accessibility information * * @param theme - Theme configuration * @returns Accessibility info lines */ private generateAccessibilityInfo(theme: Theme): string[] { const lines: string[] = []; lines.push(TerminalStyle.style('Accessibility', ANSI.bold)); lines.push(''); const validation = themeRegistry.validate(theme); // Overall compliance const wcagAA = validation.accessibility.wcagAA ? TerminalStyle.style('✓ WCAG AA', ANSI.green) : TerminalStyle.style('✗ WCAG AA', ANSI.red); const wcagAAA = validation.accessibility.wcagAAA ? TerminalStyle.style('✓ WCAG AAA', ANSI.green) : TerminalStyle.style('✗ WCAG AAA', ANSI.red); lines.push(`Compliance: ${wcagAA} • ${wcagAAA}`); // Contrast issues if (validation.accessibility.contrastIssues.length > 0) { lines.push(''); lines.push(TerminalStyle.style('Contrast Issues:', ANSI.bold, ANSI.yellow)); for (const issue of validation.accessibility.contrastIssues.slice(0, 5)) { // Show max 5 const ratio = issue.ratio.toFixed(1); const required = issue.required.toFixed(1); const level = TerminalStyle.style(issue.level, ANSI.red); lines.push(` • ${issue.pair[0]}/${issue.pair[1]}: ${ratio}:1 (need ${required}:1 for ${level})`); } if (validation.accessibility.contrastIssues.length > 5) { const remaining = validation.accessibility.contrastIssues.length - 5; lines.push(TerminalStyle.style(` ... and ${remaining} more issues`, ANSI.dim)); } } return lines; } /** * Generate theme comparison * * @param themes - Themes to compare * @returns Comparison string */ generateComparison(themes: Theme[]): string { const lines: string[] = []; lines.push(TerminalStyle.style('Theme Comparison', ANSI.bold, ANSI.underline)); lines.push(''); // Create table header const nameCol = 'Name'.padEnd(15); const colorCol = 'Colors'.padEnd(10); const accessCol = 'WCAG'.padEnd(8); const tagsCol = 'Tags'; lines.push(TerminalStyle.style(`${nameCol} ${colorCol} ${accessCol} ${tagsCol}`, ANSI.bold)); lines.push('─'.repeat(this.options.width)); // Add theme rows for (const theme of themes) { const validation = themeRegistry.validate(theme); const name = theme.name.substring(0, 14).padEnd(15); const colors = theme.colorScheme.substring(0, 9).padEnd(10); const wcag = validation.accessibility.wcagAA ? TerminalStyle.style('AA ✓'.padEnd(8), ANSI.green) : TerminalStyle.style('AA ✗'.padEnd(8), ANSI.red); const tags = theme.meta.tags.slice(0, 3).join(', '); lines.push(`${name} ${colors} ${wcag} ${tags}`); } return lines.join('\n'); } /** * Generate theme list * * @param themes - Themes to list * @returns Theme list string */ generateList(themes: Theme[]): string { const lines: string[] = []; for (let i = 0; i < themes.length; i++) { const theme = themes[i]; const validation = themeRegistry.validate(theme); const number = TerminalStyle.style(`${i + 1}.`.padEnd(4), ANSI.dim); const name = TerminalStyle.style(theme.name, ANSI.bold); const scheme = TerminalStyle.style(`(${theme.colorScheme})`, ANSI.cyan); const wcag = validation.accessibility.wcagAA ? TerminalStyle.style(' ✓', ANSI.green) : TerminalStyle.style(' ✗', ANSI.red); lines.push(`${number} ${name} ${scheme}${wcag}`); if (!this.options.compact) { const description = TerminalStyle.style(` ${theme.meta.description}`, ANSI.dim); lines.push(description); } } return lines.join('\n'); } } /** * Convenience functions */ /** * Preview a theme by ID * * @param themeId - Theme identifier * @param options - Preview options * @returns Preview string */ export function previewTheme(themeId: string, options?: PreviewOptions): string { const theme = themeRegistry.get(themeId); if (!theme) { return TerminalStyle.style(`Theme '${themeId}' not found`, ANSI.red); } const preview = new ThemePreview(options); return preview.generatePreview(theme); } /** * List all available themes * * @param options - Preview options * @returns Theme list string */ export function listThemes(options?: PreviewOptions): string { const themes = themeRegistry.getAll(); const preview = new ThemePreview(options); return preview.generateList(themes); } /** * Compare multiple themes * * @param themeIds - Theme identifiers to compare * @param options - Preview options * @returns Comparison string */ export function compareThemes(themeIds: string[], options?: PreviewOptions): string { const themes = themeIds .map(id => themeRegistry.get(id)) .filter(Boolean) as Theme[]; const preview = new ThemePreview(options); return preview.generateComparison(themes); } /** * Search and preview themes * * @param query - Search query * @param options - Preview options * @returns Search results with previews */ export function searchAndPreviewThemes(query: string, options?: PreviewOptions): string { const themes = themeRegistry.search(query); if (themes.length === 0) { return TerminalStyle.style(`No themes found matching '${query}'`, ANSI.yellow); } const preview = new ThemePreview(options); const header = TerminalStyle.style(`Found ${themes.length} theme(s) matching '${query}':`, ANSI.bold); const list = preview.generateList(themes); return `${header}\n\n${list}`; }