UNPKG

revolutionary-ui

Version:

Revolutionary UI v3.0 - AI-Powered Interactive CLI with 10+ AI providers, website inspiration analyzer, and advanced code generation

1,108 lines (1,027 loc) 36.6 kB
"use strict"; /** * Component Generator for Revolutionary UI Factory * Generates framework-specific components with massive code reduction */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ComponentGenerator = void 0; const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); const chalk_1 = __importDefault(require("chalk")); class ComponentGenerator { templates = new Map(); constructor() { this.initializeTemplates(); } /** * Generate a component based on options */ async generate(options) { // Validate options const validation = this.validateOptions(options); if (!validation.valid) { throw new Error(`Invalid options: ${validation.errors.join(', ')}`); } // Get template const templateKey = `${options.framework}-${options.type}`; const template = this.templates.get(templateKey) || this.templates.get(`generic-${options.type}`); if (!template) { throw new Error(`No template found for ${options.type} component`); } // Generate component code const code = await this.generateCode(template, options); // Generate additional files (styles, tests, etc.) const files = [ { filename: this.getFilename(options), content: code.component, type: 'component' } ]; if (code.styles) { files.push({ filename: this.getStyleFilename(options), content: code.styles, type: 'styles' }); } if (code.test) { files.push({ filename: this.getTestFilename(options), content: code.test, type: 'test' }); } if (code.story) { files.push({ filename: this.getStoryFilename(options), content: code.story, type: 'story' }); } // Calculate metrics const metrics = this.calculateMetrics(code.component, options.type); return { files, metrics, preview: code.component.substring(0, 500) + '...', instructions: this.getUsageInstructions(options) }; } /** * Save generated files to disk */ async saveFiles(result, baseDir) { for (const file of result.files) { const filePath = path.join(baseDir, file.filename); const dir = path.dirname(filePath); // Ensure directory exists await fs.mkdir(dir, { recursive: true }); // Write file await fs.writeFile(filePath, file.content, 'utf-8'); console.log(chalk_1.default.green(`✅ Created ${file.filename}`)); } } /** * Initialize component templates */ initializeTemplates() { // React templates this.templates.set('react-button', { generate: (opts) => this.generateReactButton(opts) }); this.templates.set('react-form', { generate: (opts) => this.generateReactForm(opts) }); this.templates.set('react-table', { generate: (opts) => this.generateReactTable(opts) }); this.templates.set('react-dashboard', { generate: (opts) => this.generateReactDashboard(opts) }); // Vue templates this.templates.set('vue-button', { generate: (opts) => this.generateVueButton(opts) }); this.templates.set('vue-form', { generate: (opts) => this.generateVueForm(opts) }); // Add more framework-specific templates... // Generic templates (framework-agnostic) this.templates.set('generic-button', { generate: (opts) => this.generateGenericButton(opts) }); } /** * Generate code using template */ async generateCode(template, options) { return await template.generate(options); } /** * React Button Generator */ async generateReactButton(options) { const { name, typescript, styling, props = {} } = options; const ext = typescript ? 'tsx' : 'jsx'; let imports = `import React from 'react';\n`; let styles = ''; let componentCode = ''; if (styling === 'tailwind') { componentCode = ` ${typescript ? `interface ${name}Props { variant?: 'primary' | 'secondary' | 'danger'; size?: 'sm' | 'md' | 'lg'; disabled?: boolean; loading?: boolean; children: React.ReactNode; onClick?: () => void; className?: string; }` : ''} export const ${name}${typescript ? ': React.FC<' + name + 'Props>' : ''} = ({ variant = 'primary', size = 'md', disabled = false, loading = false, children, onClick, className = '', ...props }) => { const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2'; const variantClasses = { primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500', secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500', danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500' }; const sizeClasses = { sm: 'px-3 py-1.5 text-sm', md: 'px-4 py-2 text-base', lg: 'px-6 py-3 text-lg' }; const disabledClasses = disabled || loading ? 'opacity-50 cursor-not-allowed' : ''; return ( <button className={\`\${baseClasses} \${variantClasses[variant]} \${sizeClasses[size]} \${disabledClasses} \${className}\`} disabled={disabled || loading} onClick={onClick} {...props} > {loading && ( <svg className="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> )} {children} </button> ); };`; } else if (styling === 'styled-components') { imports += `import styled from 'styled-components';\n`; componentCode = ` const StyledButton = styled.button<{ $variant: string; $size: string }>\` display: inline-flex; align-items: center; justify-content: center; font-weight: 500; border-radius: 0.5rem; transition: all 0.2s; border: none; cursor: pointer; &:focus { outline: none; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5); } \${props => props.$variant === 'primary' && \` background-color: #2563eb; color: white; &:hover { background-color: #1d4ed8; } \`} \${props => props.$variant === 'secondary' && \` background-color: #e5e7eb; color: #111827; &:hover { background-color: #d1d5db; } \`} \${props => props.$size === 'sm' && \` padding: 0.375rem 0.75rem; font-size: 0.875rem; \`} \${props => props.$size === 'md' && \` padding: 0.5rem 1rem; font-size: 1rem; \`} &:disabled { opacity: 0.5; cursor: not-allowed; } \`; export const ${name} = ({ variant = 'primary', size = 'md', ...props }) => { return <StyledButton $variant={variant} $size={size} {...props} />; };`; } const component = imports + componentCode; // Generate test const test = ` import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { ${name} } from './${name}'; describe('${name}', () => { it('renders children correctly', () => { render(<${name}>Click me</${name}>); expect(screen.getByText('Click me')).toBeInTheDocument(); }); it('handles click events', () => { const handleClick = jest.fn(); render(<${name} onClick={handleClick}>Click me</${name}>); fireEvent.click(screen.getByText('Click me')); expect(handleClick).toHaveBeenCalledTimes(1); }); it('is disabled when disabled prop is true', () => { render(<${name} disabled>Click me</${name}>); expect(screen.getByText('Click me')).toBeDisabled(); }); });`; // Generate Storybook story const story = ` import React from 'react'; import { ${name} } from './${name}'; export default { title: 'Components/${name}', component: ${name}, }; export const Primary = () => <${name} variant="primary">Primary Button</${name}>; export const Secondary = () => <${name} variant="secondary">Secondary Button</${name}>; export const Danger = () => <${name} variant="danger">Danger Button</${name}>; export const Sizes = () => ( <div className="space-x-4"> <${name} size="sm">Small</${name}> <${name} size="md">Medium</${name}> <${name} size="lg">Large</${name}> </div> ); export const Loading = () => <${name} loading>Loading...</${name}>; export const Disabled = () => <${name} disabled>Disabled</${name}>;`; return { component, styles, test, story }; } /** * React Form Generator */ async generateReactForm(options) { const { name, typescript, styling, props = {} } = options; let imports = `import React, { useState } from 'react';\n`; if (typescript) { imports += ` interface ${name}Data { name: string; email: string; message: string; } interface ${name}Props { onSubmit: (data: ${name}Data) => void; className?: string; }`; } const component = `${imports} export const ${name}${typescript ? ': React.FC<' + name + 'Props>' : ''} = ({ onSubmit, className = '' }) => { const [formData, setFormData] = useState${typescript ? '<' + name + 'Data>' : ''}({ name: '', email: '', message: '' }); const [errors, setErrors] = useState${typescript ? '<Partial<' + name + 'Data>>' : ''}({}); const [isSubmitting, setIsSubmitting] = useState(false); const handleChange = (e${typescript ? ': React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>' : ''}) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); // Clear error when user types if (errors[name]) { setErrors(prev => ({ ...prev, [name]: '' })); } }; const validate = () => { const newErrors${typescript ? ': Partial<' + name + 'Data>' : ''} = {}; if (!formData.name.trim()) { newErrors.name = 'Name is required'; } if (!formData.email.trim()) { newErrors.email = 'Email is required'; } else if (!/\\S+@\\S+\\.\\S+/.test(formData.email)) { newErrors.email = 'Email is invalid'; } if (!formData.message.trim()) { newErrors.message = 'Message is required'; } return newErrors; }; const handleSubmit = async (e${typescript ? ': React.FormEvent' : ''}) => { e.preventDefault(); const validationErrors = validate(); if (Object.keys(validationErrors).length > 0) { setErrors(validationErrors); return; } setIsSubmitting(true); try { await onSubmit(formData); // Reset form on success setFormData({ name: '', email: '', message: '' }); } catch (error) { console.error('Form submission error:', error); } finally { setIsSubmitting(false); } }; return ( <form onSubmit={handleSubmit} className={\`space-y-4 \${className}\`}> <div> <label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1"> Name </label> <input type="text" id="name" name="name" value={formData.name} onChange={handleChange} className={\`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 \${ errors.name ? 'border-red-500' : 'border-gray-300' }\`} disabled={isSubmitting} /> {errors.name && ( <p className="mt-1 text-sm text-red-600">{errors.name}</p> )} </div> <div> <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1"> Email </label> <input type="email" id="email" name="email" value={formData.email} onChange={handleChange} className={\`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 \${ errors.email ? 'border-red-500' : 'border-gray-300' }\`} disabled={isSubmitting} /> {errors.email && ( <p className="mt-1 text-sm text-red-600">{errors.email}</p> )} </div> <div> <label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-1"> Message </label> <textarea id="message" name="message" rows={4} value={formData.message} onChange={handleChange} className={\`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 \${ errors.message ? 'border-red-500' : 'border-gray-300' }\`} disabled={isSubmitting} /> {errors.message && ( <p className="mt-1 text-sm text-red-600">{errors.message}</p> )} </div> <button type="submit" disabled={isSubmitting} className="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" > {isSubmitting ? 'Submitting...' : 'Submit'} </button> </form> ); };`; return { component }; } /** * React Table Generator */ async generateReactTable(options) { const { name, typescript, features = [] } = options; const hasSort = features.includes('sort'); const hasFilter = features.includes('filter'); const hasPagination = features.includes('pagination'); const hasSelection = features.includes('selection'); let imports = `import React, { useState, useMemo } from 'react';\n`; if (typescript) { imports += ` interface Column<T> { key: keyof T; header: string; sortable?: boolean; render?: (value: any, row: T) => React.ReactNode; } interface ${name}Props<T> { data: T[]; columns: Column<T>[]; onRowClick?: (row: T) => void; onSelectionChange?: (selected: T[]) => void; className?: string; }`; } const component = `${imports} export const ${name} = ${typescript ? '<T extends Record<string, any>>' : ''}({ data, columns, onRowClick, onSelectionChange, className = '' }${typescript ? ': ' + name + 'Props<T>' : ''}) => { const [sortKey, setSortKey] = useState${typescript ? '<keyof T | null>' : ''}(null); const [sortOrder, setSortOrder] = useState${typescript ? '<"asc" | "desc">' : ''}('asc'); const [filterValue, setFilterValue] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [selectedRows, setSelectedRows] = useState${typescript ? '<T[]>' : ''}([]); const itemsPerPage = 10; // Filtering const filteredData = useMemo(() => { if (!filterValue) return data; return data.filter(row => Object.values(row).some(value => String(value).toLowerCase().includes(filterValue.toLowerCase()) ) ); }, [data, filterValue]); // Sorting const sortedData = useMemo(() => { if (!sortKey) return filteredData; return [...filteredData].sort((a, b) => { const aVal = a[sortKey]; const bVal = b[sortKey]; if (aVal < bVal) return sortOrder === 'asc' ? -1 : 1; if (aVal > bVal) return sortOrder === 'asc' ? 1 : -1; return 0; }); }, [filteredData, sortKey, sortOrder]); // Pagination const paginatedData = useMemo(() => { const start = (currentPage - 1) * itemsPerPage; return sortedData.slice(start, start + itemsPerPage); }, [sortedData, currentPage]); const totalPages = Math.ceil(sortedData.length / itemsPerPage); const handleSort = (key${typescript ? ': keyof T' : ''}) => { if (sortKey === key) { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); } else { setSortKey(key); setSortOrder('asc'); } }; const handleSelectAll = () => { if (selectedRows.length === paginatedData.length) { setSelectedRows([]); } else { setSelectedRows(paginatedData); } onSelectionChange?.(selectedRows); }; const handleSelectRow = (row${typescript ? ': T' : ''}) => { const isSelected = selectedRows.some(r => r === row); const newSelected = isSelected ? selectedRows.filter(r => r !== row) : [...selectedRows, row]; setSelectedRows(newSelected); onSelectionChange?.(newSelected); }; return ( <div className={\`\${className}\`}> ${hasFilter ? `<div className="mb-4"> <input type="text" placeholder="Search..." value={filterValue} onChange={(e) => setFilterValue(e.target.value)} className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" /> </div>` : ''} <div className="overflow-x-auto"> <table className="min-w-full divide-y divide-gray-200"> <thead className="bg-gray-50"> <tr> ${hasSelection ? `<th className="px-6 py-3 text-left"> <input type="checkbox" checked={selectedRows.length === paginatedData.length && paginatedData.length > 0} onChange={handleSelectAll} className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" /> </th>` : ''} {columns.map(column => ( <th key={String(column.key)} className={\`px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider \${ column.sortable ? 'cursor-pointer hover:bg-gray-100' : '' }\`} onClick={() => column.sortable && handleSort(column.key)} > <div className="flex items-center space-x-1"> <span>{column.header}</span> {column.sortable && sortKey === column.key && ( <span>{sortOrder === 'asc' ? '↑' : '↓'}</span> )} </div> </th> ))} </tr> </thead> <tbody className="bg-white divide-y divide-gray-200"> {paginatedData.map((row, index) => ( <tr key={index} onClick={() => onRowClick?.(row)} className={\`\${onRowClick ? 'cursor-pointer hover:bg-gray-50' : ''}\`} > ${hasSelection ? `<td className="px-6 py-4"> <input type="checkbox" checked={selectedRows.includes(row)} onChange={(e) => { e.stopPropagation(); handleSelectRow(row); }} className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" /> </td>` : ''} {columns.map(column => ( <td key={String(column.key)} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> {column.render ? column.render(row[column.key], row) : row[column.key]} </td> ))} </tr> ))} </tbody> </table> </div> ${hasPagination ? `<div className="mt-4 flex items-center justify-between"> <div className="text-sm text-gray-700"> Showing {(currentPage - 1) * itemsPerPage + 1} to {Math.min(currentPage * itemsPerPage, sortedData.length)} of {sortedData.length} results </div> <div className="flex space-x-2"> <button onClick={() => setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} className="px-3 py-1 border border-gray-300 rounded-md disabled:opacity-50 disabled:cursor-not-allowed" > Previous </button> <span className="px-3 py-1"> Page {currentPage} of {totalPages} </span> <button onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages} className="px-3 py-1 border border-gray-300 rounded-md disabled:opacity-50 disabled:cursor-not-allowed" > Next </button> </div> </div>` : ''} </div> ); };`; return { component }; } /** * React Dashboard Generator */ async generateReactDashboard(options) { const { name, typescript } = options; const component = `import React from 'react'; ${typescript ? `interface StatCard { title: string; value: string | number; change?: number; icon?: React.ReactNode; } interface ${name}Props { stats: StatCard[]; className?: string; }` : ''} export const ${name}${typescript ? ': React.FC<' + name + 'Props>' : ''} = ({ stats, className = '' }) => { return ( <div className={\`\${className}\`}> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> {stats.map((stat, index) => ( <div key={index} className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <div className="flex items-center justify-between"> <div> <p className="text-sm font-medium text-gray-600">{stat.title}</p> <p className="text-2xl font-bold text-gray-900 mt-2">{stat.value}</p> {stat.change !== undefined && ( <p className={\`text-sm mt-2 \${stat.change >= 0 ? 'text-green-600' : 'text-red-600'}\`}> {stat.change >= 0 ? '+' : ''}{stat.change}% </p> )} </div> {stat.icon && ( <div className="text-gray-400">{stat.icon}</div> )} </div> </div> ))} </div> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mt-6"> <div className="lg:col-span-2 bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <h3 className="text-lg font-semibold text-gray-900 mb-4">Recent Activity</h3> <div className="space-y-4"> {/* Activity items would go here */} <p className="text-gray-500">No recent activity</p> </div> </div> <div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <h3 className="text-lg font-semibold text-gray-900 mb-4">Quick Actions</h3> <div className="space-y-2"> <button className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"> Create New </button> <button className="w-full px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"> View Reports </button> <button className="w-full px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"> Settings </button> </div> </div> </div> </div> ); };`; return { component }; } /** * Vue Button Generator */ async generateVueButton(options) { const { name, typescript } = options; const component = `<template> <button :class="buttonClasses" :disabled="disabled || loading" @click="$emit('click')" > <svg v-if="loading" class="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> <slot /> </button> </template> <script${typescript ? ' lang="ts"' : ''}> ${typescript ? `import { defineComponent, PropType } from 'vue'; export default defineComponent({ name: '${name}',` : `export default { name: '${name}',`} props: { variant: { type: String${typescript ? ' as PropType<"primary" | "secondary" | "danger">' : ''}, default: 'primary', validator: (value${typescript ? ': string' : ''}) => ['primary', 'secondary', 'danger'].includes(value) }, size: { type: String${typescript ? ' as PropType<"sm" | "md" | "lg">' : ''}, default: 'md', validator: (value${typescript ? ': string' : ''}) => ['sm', 'md', 'lg'].includes(value) }, disabled: { type: Boolean, default: false }, loading: { type: Boolean, default: false } }, emits: ['click'], computed: { buttonClasses() { const base = 'inline-flex items-center justify-center font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2'; const variants = { primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500', secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500', danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500' }; const sizes = { sm: 'px-3 py-1.5 text-sm', md: 'px-4 py-2 text-base', lg: 'px-6 py-3 text-lg' }; const disabled = this.disabled || this.loading ? 'opacity-50 cursor-not-allowed' : ''; return \`\${base} \${variants[this.variant]} \${sizes[this.size]} \${disabled}\`; } } }${typescript ? ')' : ''}; </script>`; return { component }; } /** * Vue Form Generator */ async generateVueForm(options) { const { name, typescript } = options; const component = `<template> <form @submit.prevent="handleSubmit" :class="className"> <div class="space-y-4"> <div> <label for="name" class="block text-sm font-medium text-gray-700 mb-1"> Name </label> <input type="text" id="name" v-model="formData.name" :class="inputClasses(errors.name)" :disabled="isSubmitting" /> <p v-if="errors.name" class="mt-1 text-sm text-red-600">{{ errors.name }}</p> </div> <div> <label for="email" class="block text-sm font-medium text-gray-700 mb-1"> Email </label> <input type="email" id="email" v-model="formData.email" :class="inputClasses(errors.email)" :disabled="isSubmitting" /> <p v-if="errors.email" class="mt-1 text-sm text-red-600">{{ errors.email }}</p> </div> <div> <label for="message" class="block text-sm font-medium text-gray-700 mb-1"> Message </label> <textarea id="message" v-model="formData.message" rows="4" :class="inputClasses(errors.message)" :disabled="isSubmitting" /> <p v-if="errors.message" class="mt-1 text-sm text-red-600">{{ errors.message }}</p> </div> <button type="submit" :disabled="isSubmitting" class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" > {{ isSubmitting ? 'Submitting...' : 'Submit' }} </button> </div> </form> </template> <script${typescript ? ' lang="ts"' : ''}> ${typescript ? `import { defineComponent, reactive, ref } from 'vue'; interface FormData { name: string; email: string; message: string; } export default defineComponent({ name: '${name}',` : `import { reactive, ref } from 'vue'; export default { name: '${name}',`} props: { className: { type: String, default: '' } }, emits: ['submit'], setup(props, { emit }) { const formData = reactive${typescript ? '<FormData>' : ''}({ name: '', email: '', message: '' }); const errors = reactive${typescript ? '<Partial<FormData>>' : ''}({}); const isSubmitting = ref(false); const inputClasses = (error${typescript ? '?: string' : ''}) => { const base = 'w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500'; return \`\${base} \${error ? 'border-red-500' : 'border-gray-300'}\`; }; const validate = () => { const newErrors${typescript ? ': Partial<FormData>' : ''} = {}; if (!formData.name.trim()) { newErrors.name = 'Name is required'; } if (!formData.email.trim()) { newErrors.email = 'Email is required'; } else if (!/\\S+@\\S+\\.\\S+/.test(formData.email)) { newErrors.email = 'Email is invalid'; } if (!formData.message.trim()) { newErrors.message = 'Message is required'; } Object.assign(errors, newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = async () => { if (!validate()) return; isSubmitting.value = true; try { await emit('submit', { ...formData }); // Reset form Object.assign(formData, { name: '', email: '', message: '' }); Object.keys(errors).forEach(key => delete errors[key]); } catch (error) { console.error('Form submission error:', error); } finally { isSubmitting.value = false; } }; return { formData, errors, isSubmitting, inputClasses, handleSubmit }; } }${typescript ? ')' : ''}; </script>`; return { component }; } /** * Generic Button Generator */ async generateGenericButton(options) { const { framework = 'react' } = options; // For unknown frameworks, generate React by default return this.generateReactButton(options); } /** * Validate component options */ validateOptions(options) { const errors = []; if (!options.name) { errors.push('Component name is required'); } if (!options.type) { errors.push('Component type is required'); } if (options.name && !/^[A-Z][a-zA-Z0-9]*$/.test(options.name)) { errors.push('Component name must start with capital letter and contain only letters and numbers'); } return { valid: errors.length === 0, errors }; } /** * Get filename for component */ getFilename(options) { const { name, framework = 'react', typescript } = options; const ext = this.getExtension(framework, typescript); return `${name}.${ext}`; } /** * Get style filename */ getStyleFilename(options) { const { name, styling = 'css' } = options; const ext = styling === 'scss' || styling === 'sass' ? styling : 'css'; return `${name}.${ext}`; } /** * Get test filename */ getTestFilename(options) { const { name, framework = 'react', typescript } = options; const ext = typescript ? 'test.ts' : 'test.js'; return `${name}.${ext}`; } /** * Get story filename */ getStoryFilename(options) { const { name, framework = 'react', typescript } = options; const ext = typescript ? 'stories.tsx' : 'stories.jsx'; return `${name}.${ext}`; } /** * Get file extension based on framework and typescript */ getExtension(framework, typescript) { const extensions = { react: { ts: 'tsx', js: 'jsx' }, vue: { ts: 'vue', js: 'vue' }, angular: { ts: 'ts', js: 'js' }, svelte: { ts: 'svelte', js: 'svelte' }, solid: { ts: 'tsx', js: 'jsx' }, preact: { ts: 'tsx', js: 'jsx' }, alpine: { ts: 'ts', js: 'js' }, lit: { ts: 'ts', js: 'js' }, qwik: { ts: 'tsx', js: 'jsx' }, astro: { ts: 'astro', js: 'astro' } }; const ext = extensions[framework] || { ts: 'ts', js: 'js' }; return typescript ? ext.ts : ext.js; } /** * Calculate code reduction metrics */ calculateMetrics(code, type) { const lines = code.split('\n').length; // Estimated traditional lines based on component type const traditionalLines = { button: 150, form: 400, table: 800, card: 200, modal: 300, dashboard: 1000, kanban: 600, calendar: 800, chart: 500, list: 300, grid: 400, tabs: 250, accordion: 300, menu: 350, navbar: 400, sidebar: 450, footer: 200, hero: 300, pricing: 500 }; const traditional = traditionalLines[type] || 300; const reduction = Math.round((1 - lines / traditional) * 100); return { linesGenerated: lines, linesTraditional: traditional, codeReduction: `${reduction}%`, timesSaved: `${Math.round(traditional / lines)}x faster` }; } /** * Get usage instructions */ getUsageInstructions(options) { const { name, framework = 'react' } = options; const instructions = []; if (framework === 'react') { instructions.push(`Import: import { ${name} } from './components/${name}'`); instructions.push(`Usage: <${name} />`); } else if (framework === 'vue') { instructions.push(`Import in script: import ${name} from './components/${name}.vue'`); instructions.push(`Register: components: { ${name} }`); instructions.push(`Usage: <${name} />`); } instructions.push(`Documentation: See ${name}.stories.js for examples`); instructions.push(`Tests: Run 'npm test ${name}.test.js'`); return instructions; } } exports.ComponentGenerator = ComponentGenerator; //# sourceMappingURL=component-generator.js.map