UNPKG

prprompts-flutter-generator

Version:

AI-powered Flutter development with full automation + official extension support - Generate 32 security-audited guides & auto-implement in 2-3 hours. NEW v5.1: Official Claude Code plugin with hooks, Gemini TOML commands, Qwen MCP settings. Features: Comp

920 lines (810 loc) 27.4 kB
/** * Style Converter - Convert React/CSS styles to Flutter styling * * Handles: * - Color conversion (named, hex, rgb, rgba) * - Dimension conversion (px, %, rem → logical pixels) * - BoxDecoration (background, border, shadow, radius) * - EdgeInsets (padding, margin) * - TextStyle (font, size, weight, color, height) * - Flexbox (alignment, direction) */ const { createModuleLogger } = require('../../utils/logger'); const logger = createModuleLogger('StyleConverter'); /** * Named color map for CSS color names to Flutter Colors */ const NAMED_COLORS = { // Basic colors 'transparent': 'Colors.transparent', 'black': 'Colors.black', 'white': 'Colors.white', 'red': 'Colors.red', 'green': 'Colors.green', 'blue': 'Colors.blue', 'yellow': 'Colors.yellow', 'orange': 'Colors.orange', 'purple': 'Colors.purple', 'pink': 'Colors.pink', 'brown': 'Colors.brown', 'grey': 'Colors.grey', 'gray': 'Colors.grey', 'cyan': 'Colors.cyan', 'indigo': 'Colors.indigo', 'lime': 'Colors.lime', 'teal': 'Colors.teal', 'amber': 'Colors.amber', // Extended colors 'lightblue': 'Colors.lightBlue', 'lightgreen': 'Colors.lightGreen', 'deeporange': 'Colors.deepOrange', 'deeppurple': 'Colors.deepPurple', 'bluegrey': 'Colors.blueGrey', 'bluegray': 'Colors.blueGrey', }; /** * Font weight map from CSS to Flutter */ const FONT_WEIGHTS = { 'normal': 'FontWeight.normal', '400': 'FontWeight.normal', 'bold': 'FontWeight.bold', '700': 'FontWeight.bold', '100': 'FontWeight.w100', '200': 'FontWeight.w200', '300': 'FontWeight.w300', '500': 'FontWeight.w500', '600': 'FontWeight.w600', '800': 'FontWeight.w800', '900': 'FontWeight.w900', }; /** * Text align map from CSS to Flutter */ const TEXT_ALIGN = { 'left': 'TextAlign.left', 'right': 'TextAlign.right', 'center': 'TextAlign.center', 'justify': 'TextAlign.justify', 'start': 'TextAlign.start', 'end': 'TextAlign.end', }; /** * Main axis alignment (justifyContent in CSS) */ const JUSTIFY_CONTENT = { 'flex-start': 'MainAxisAlignment.start', 'flex-end': 'MainAxisAlignment.end', 'center': 'MainAxisAlignment.center', 'space-between': 'MainAxisAlignment.spaceBetween', 'space-around': 'MainAxisAlignment.spaceAround', 'space-evenly': 'MainAxisAlignment.spaceEvenly', }; /** * Cross axis alignment (alignItems in CSS) */ const ALIGN_ITEMS = { 'flex-start': 'CrossAxisAlignment.start', 'flex-end': 'CrossAxisAlignment.end', 'center': 'CrossAxisAlignment.center', 'stretch': 'CrossAxisAlignment.stretch', 'baseline': 'CrossAxisAlignment.baseline', }; /** * StyleConverter class - Main style conversion engine */ class StyleConverter { /** * Convert React/CSS style object to Flutter properties * @param {Object} styleObject - Style object with CSS properties * @param {string} widgetType - Type of widget ('Container', 'Text', etc.) * @returns {Object} Flutter properties */ convertStyleObject(styleObject, widgetType = 'Container') { if (!styleObject || typeof styleObject !== 'object') { return {}; } logger.debug('Converting style object', { styleObject, widgetType }); const flutterStyle = { decoration: {}, padding: null, margin: null, width: null, height: null, textStyle: {}, alignment: null, constraints: null, transform: null, flex: null, mainAxisAlignment: null, crossAxisAlignment: null, }; // Process each style property for (const [key, value] of Object.entries(styleObject)) { this._applyStyleProperty(key, value, flutterStyle); } // Build final Flutter properties return this._buildFlutterProperties(flutterStyle, widgetType); } /** * Apply a single style property to Flutter style object * @param {string} key - CSS property name * @param {*} value - CSS property value * @param {Object} flutterStyle - Accumulator for Flutter properties * @private */ _applyStyleProperty(key, value, flutterStyle) { switch (key) { // Background color case 'backgroundColor': case 'background-color': flutterStyle.decoration.color = this._convertColor(value); break; // Padding case 'padding': flutterStyle.padding = this._convertPadding(value); break; case 'paddingTop': case 'padding-top': flutterStyle.padding = this._mergePadding(flutterStyle.padding, 'top', value); break; case 'paddingBottom': case 'padding-bottom': flutterStyle.padding = this._mergePadding(flutterStyle.padding, 'bottom', value); break; case 'paddingLeft': case 'padding-left': flutterStyle.padding = this._mergePadding(flutterStyle.padding, 'left', value); break; case 'paddingRight': case 'padding-right': flutterStyle.padding = this._mergePadding(flutterStyle.padding, 'right', value); break; // Margin case 'margin': flutterStyle.margin = this._convertPadding(value); break; case 'marginTop': case 'margin-top': flutterStyle.margin = this._mergePadding(flutterStyle.margin, 'top', value); break; case 'marginBottom': case 'margin-bottom': flutterStyle.margin = this._mergePadding(flutterStyle.margin, 'bottom', value); break; case 'marginLeft': case 'margin-left': flutterStyle.margin = this._mergePadding(flutterStyle.margin, 'left', value); break; case 'marginRight': case 'margin-right': flutterStyle.margin = this._mergePadding(flutterStyle.margin, 'right', value); break; // Dimensions case 'width': flutterStyle.width = this._convertDimension(value); break; case 'height': flutterStyle.height = this._convertDimension(value); break; case 'minWidth': case 'min-width': flutterStyle.constraints = flutterStyle.constraints || {}; flutterStyle.constraints.minWidth = this._convertDimension(value); break; case 'maxWidth': case 'max-width': flutterStyle.constraints = flutterStyle.constraints || {}; flutterStyle.constraints.maxWidth = this._convertDimension(value); break; case 'minHeight': case 'min-height': flutterStyle.constraints = flutterStyle.constraints || {}; flutterStyle.constraints.minHeight = this._convertDimension(value); break; case 'maxHeight': case 'max-height': flutterStyle.constraints = flutterStyle.constraints || {}; flutterStyle.constraints.maxHeight = this._convertDimension(value); break; // Border case 'borderRadius': case 'border-radius': flutterStyle.decoration.borderRadius = this._convertBorderRadius(value); break; case 'borderWidth': case 'border-width': flutterStyle.decoration.border = this._convertBorder(value, null); break; case 'borderColor': case 'border-color': flutterStyle.decoration.borderColor = this._convertColor(value); break; case 'border': const borderParts = this._parseBorderShorthand(value); if (borderParts) { flutterStyle.decoration.border = this._convertBorder( borderParts.width, borderParts.color ); } break; // Shadow case 'boxShadow': case 'box-shadow': flutterStyle.decoration.boxShadow = this._convertBoxShadow(value); break; // Text styles case 'fontSize': case 'font-size': flutterStyle.textStyle.fontSize = this._convertFontSize(value); break; case 'fontWeight': case 'font-weight': flutterStyle.textStyle.fontWeight = this._convertFontWeight(value); break; case 'fontFamily': case 'font-family': flutterStyle.textStyle.fontFamily = this._convertFontFamily(value); break; case 'color': flutterStyle.textStyle.color = this._convertColor(value); break; case 'textAlign': case 'text-align': flutterStyle.textAlign = this._convertTextAlign(value); break; case 'lineHeight': case 'line-height': flutterStyle.textStyle.height = this._convertLineHeight(value); break; case 'letterSpacing': case 'letter-spacing': flutterStyle.textStyle.letterSpacing = this._convertDimension(value); break; case 'textDecoration': case 'text-decoration': flutterStyle.textStyle.decoration = this._convertTextDecoration(value); break; // Flexbox case 'flex': flutterStyle.flex = parseInt(value, 10); break; case 'flexDirection': case 'flex-direction': // This affects parent widget choice (Row vs Column) flutterStyle.flexDirection = value; break; case 'justifyContent': case 'justify-content': flutterStyle.mainAxisAlignment = this._convertJustifyContent(value); break; case 'alignItems': case 'align-items': flutterStyle.crossAxisAlignment = this._convertAlignItems(value); break; // Alignment case 'alignSelf': case 'align-self': flutterStyle.alignment = this._convertAlignSelf(value); break; // Opacity case 'opacity': flutterStyle.opacity = parseFloat(value); break; // Transform case 'transform': flutterStyle.transform = this._convertTransform(value); break; // Position case 'position': flutterStyle.position = value; break; case 'top': flutterStyle.top = this._convertDimension(value); break; case 'bottom': flutterStyle.bottom = this._convertDimension(value); break; case 'left': flutterStyle.left = this._convertDimension(value); break; case 'right': flutterStyle.right = this._convertDimension(value); break; case 'zIndex': case 'z-index': flutterStyle.zIndex = parseInt(value, 10); break; default: logger.debug('Unsupported style property', { key, value }); } } /** * Convert color value to Flutter Color * @param {string} colorValue - CSS color value * @returns {string} Flutter Color expression * @private */ _convertColor(colorValue) { if (!colorValue) return null; const value = colorValue.toString().trim().toLowerCase(); // Named colors if (NAMED_COLORS[value]) { return NAMED_COLORS[value]; } // Hex colors (#RGB, #RRGGBB, #RRGGBBAA) if (value.startsWith('#')) { const hex = value.slice(1); let r, g, b, a = 'FF'; if (hex.length === 3) { // #RGB r = hex[0] + hex[0]; g = hex[1] + hex[1]; b = hex[2] + hex[2]; } else if (hex.length === 6) { // #RRGGBB r = hex.slice(0, 2); g = hex.slice(2, 4); b = hex.slice(4, 6); } else if (hex.length === 8) { // #RRGGBBAA r = hex.slice(0, 2); g = hex.slice(2, 4); b = hex.slice(4, 6); a = hex.slice(6, 8); } else { logger.warn('Invalid hex color format', { colorValue }); return 'Colors.transparent'; } return `Color(0x${a.toUpperCase()}${r.toUpperCase()}${g.toUpperCase()}${b.toUpperCase()})`; } // RGB/RGBA const rgbMatch = value.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/); if (rgbMatch) { const [, r, g, b, alpha = '1'] = rgbMatch; return `Color.fromRGBO(${r}, ${g}, ${b}, ${alpha})`; } // HSL/HSLA (basic support - convert to RGB) const hslMatch = value.match(/hsla?\s*\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%\s*(?:,\s*([\d.]+))?\s*\)/); if (hslMatch) { const [, h, s, l, alpha = '1'] = hslMatch; // Note: Complex HSL to RGB conversion would be needed logger.warn('HSL color conversion not fully supported', { colorValue }); return 'Colors.grey'; } logger.warn('Unknown color format', { colorValue }); return 'Colors.transparent'; } /** * Convert padding/margin value to Flutter EdgeInsets * @param {string|number} value - CSS padding value * @returns {string} Flutter EdgeInsets expression * @private */ _convertPadding(value) { if (!value) return null; const strValue = value.toString().trim(); // Single value: padding: 10 if (!strValue.includes(' ')) { const num = this._extractNumber(strValue); return `EdgeInsets.all(${num})`; } // Multiple values: padding: '10 20' or '10 20 30 40' const parts = strValue.split(/\s+/).map(p => this._extractNumber(p)); if (parts.length === 2) { // vertical horizontal return `EdgeInsets.symmetric(vertical: ${parts[0]}, horizontal: ${parts[1]})`; } else if (parts.length === 4) { // top right bottom left return `EdgeInsets.fromLTRB(${parts[3]}, ${parts[0]}, ${parts[1]}, ${parts[2]})`; } return `EdgeInsets.all(${parts[0]})`; } /** * Merge individual padding properties * @param {string} existing - Existing EdgeInsets expression * @param {string} side - 'top', 'bottom', 'left', 'right' * @param {*} value - Value to set * @returns {string} Updated EdgeInsets expression * @private */ _mergePadding(existing, side, value) { const num = this._extractNumber(value); // If no existing padding, create only() with this side if (!existing) { return `EdgeInsets.only(${side}: ${num})`; } // Parse existing EdgeInsets if (existing.includes('EdgeInsets.all')) { const allValue = existing.match(/EdgeInsets\.all\((\d+(?:\.\d+)?)\)/)?.[1] || '0'; const sides = { top: allValue, bottom: allValue, left: allValue, right: allValue, }; sides[side] = num; return `EdgeInsets.fromLTRB(${sides.left}, ${sides.top}, ${sides.right}, ${sides.bottom})`; } if (existing.includes('EdgeInsets.only')) { // Add to existing only() const sideMatch = existing.match(/EdgeInsets\.only\(([^)]+)\)/)?.[1]; if (sideMatch) { const newSides = sideMatch + `, ${side}: ${num}`; return `EdgeInsets.only(${newSides})`; } } // Fallback return `EdgeInsets.only(${side}: ${num})`; } /** * Convert dimension value (width, height, etc.) * @param {string|number} value - CSS dimension value * @returns {number|string} Flutter dimension value * @private */ _convertDimension(value) { if (typeof value === 'number') return value; if (!value) return null; const strValue = value.toString().trim(); // Remove 'px' suffix if (strValue.endsWith('px')) { return parseFloat(strValue); } // Percentage (needs context - return as is) if (strValue.endsWith('%')) { return strValue; } // Rem/em (assume 16px base) if (strValue.endsWith('rem') || strValue.endsWith('em')) { return parseFloat(strValue) * 16; } // Try to parse as number const num = parseFloat(strValue); return isNaN(num) ? null : num; } /** * Convert border radius * @param {string|number} value - CSS border-radius value * @returns {string} Flutter BorderRadius expression * @private */ _convertBorderRadius(value) { const num = this._extractNumber(value); return `BorderRadius.circular(${num})`; } /** * Convert border * @param {string|number} width - Border width * @param {string} color - Border color * @returns {string} Flutter Border expression * @private */ _convertBorder(width, color) { const widthNum = this._extractNumber(width); const colorStr = color ? this._convertColor(color) : null; if (colorStr) { return `Border.all(width: ${widthNum}, color: ${colorStr})`; } return `Border.all(width: ${widthNum})`; } /** * Parse border shorthand (e.g., '1px solid red') * @param {string} value - Border shorthand value * @returns {Object} Parsed border parts * @private */ _parseBorderShorthand(value) { const parts = value.split(/\s+/); if (parts.length < 2) return null; return { width: parts[0], style: parts[1], // solid, dashed, etc. color: parts[2] || 'black', }; } /** * Convert box shadow * @param {string} value - CSS box-shadow value * @returns {string} Flutter BoxShadow list * @private */ _convertBoxShadow(value) { if (!value || value === 'none') return null; // Parse box-shadow: offsetX offsetY blurRadius spreadRadius color // Example: '0 2px 4px 0 rgba(0,0,0,0.1)' const match = value.match(/([-\d.]+)(?:px)?\s+([-\d.]+)(?:px)?\s+([-\d.]+)(?:px)?(?:\s+([-\d.]+)(?:px)?)?\s+(.*)/); if (match) { const [, offsetX, offsetY, blurRadius, spreadRadius, color] = match; const flutterColor = this._convertColor(color || 'rgba(0,0,0,0.1)'); return `[BoxShadow(offset: Offset(${offsetX}, ${offsetY}), blurRadius: ${blurRadius}, color: ${flutterColor})]`; } return null; } /** * Convert font size * @param {string|number} value - CSS font-size value * @returns {number} Flutter font size * @private */ _convertFontSize(value) { // Use _convertDimension to handle units like px, rem, em return this._convertDimension(value); } /** * Convert font weight * @param {string|number} value - CSS font-weight value * @returns {string} Flutter FontWeight * @private */ _convertFontWeight(value) { const strValue = value.toString().trim(); return FONT_WEIGHTS[strValue] || 'FontWeight.normal'; } /** * Convert font family * @param {string} value - CSS font-family value * @returns {string} Flutter font family * @private */ _convertFontFamily(value) { // Remove quotes and take first font const cleaned = value.replace(/['"]/g, '').split(',')[0].trim(); return `'${cleaned}'`; } /** * Convert text align * @param {string} value - CSS text-align value * @returns {string} Flutter TextAlign * @private */ _convertTextAlign(value) { return TEXT_ALIGN[value] || 'TextAlign.left'; } /** * Convert line height * @param {string|number} value - CSS line-height value * @returns {number} Flutter text height multiplier * @private */ _convertLineHeight(value) { const num = parseFloat(value); return isNaN(num) ? 1.0 : num; } /** * Convert text decoration * @param {string} value - CSS text-decoration value * @returns {string} Flutter TextDecoration * @private */ _convertTextDecoration(value) { if (value.includes('underline')) return 'TextDecoration.underline'; if (value.includes('line-through')) return 'TextDecoration.lineThrough'; if (value.includes('overline')) return 'TextDecoration.overline'; return 'TextDecoration.none'; } /** * Convert justify-content * @param {string} value - CSS justify-content value * @returns {string} Flutter MainAxisAlignment * @private */ _convertJustifyContent(value) { return JUSTIFY_CONTENT[value] || 'MainAxisAlignment.start'; } /** * Convert align-items * @param {string} value - CSS align-items value * @returns {string} Flutter CrossAxisAlignment * @private */ _convertAlignItems(value) { return ALIGN_ITEMS[value] || 'CrossAxisAlignment.start'; } /** * Convert align-self * @param {string} value - CSS align-self value * @returns {string} Flutter Alignment * @private */ _convertAlignSelf(value) { const alignMap = { 'center': 'Alignment.center', 'flex-start': 'Alignment.topCenter', 'flex-end': 'Alignment.bottomCenter', }; return alignMap[value] || 'Alignment.center'; } /** * Convert transform * @param {string} value - CSS transform value * @returns {string} Flutter Matrix4 or Transform hint * @private */ _convertTransform(value) { // Basic transform support if (value.includes('scale')) { const scaleMatch = value.match(/scale\(([\d.]+)\)/); if (scaleMatch) { return `Matrix4.diagonal3Values(${scaleMatch[1]}, ${scaleMatch[1]}, 1)`; } } return null; } /** * Build final Flutter properties from style accumulator * @param {Object} flutterStyle - Accumulated Flutter style properties * @param {string} widgetType - Type of widget * @returns {Object} Final Flutter properties * @private */ _buildFlutterProperties(flutterStyle, widgetType) { const properties = {}; // BoxDecoration if (Object.keys(flutterStyle.decoration).length > 0) { properties.decoration = this._buildBoxDecoration(flutterStyle.decoration); } // Padding if (flutterStyle.padding) { properties.padding = flutterStyle.padding; } // Margin (Flutter uses Container with margin) if (flutterStyle.margin) { properties.margin = flutterStyle.margin; } // Width/Height if (flutterStyle.width !== null) { properties.width = flutterStyle.width; } if (flutterStyle.height !== null) { properties.height = flutterStyle.height; } // Constraints if (flutterStyle.constraints) { properties.constraints = this._buildConstraints(flutterStyle.constraints); } // TextStyle if (Object.keys(flutterStyle.textStyle).length > 0) { properties.style = this._buildTextStyle(flutterStyle.textStyle); } // Text align if (flutterStyle.textAlign) { properties.textAlign = flutterStyle.textAlign; } // Alignment if (flutterStyle.alignment) { properties.alignment = flutterStyle.alignment; } // Flex if (flutterStyle.flex !== null) { properties.flex = flutterStyle.flex; } // Axis alignments if (flutterStyle.mainAxisAlignment) { properties.mainAxisAlignment = flutterStyle.mainAxisAlignment; } if (flutterStyle.crossAxisAlignment) { properties.crossAxisAlignment = flutterStyle.crossAxisAlignment; } // Opacity if (flutterStyle.opacity !== undefined) { properties.opacity = flutterStyle.opacity; } // Transform if (flutterStyle.transform) { properties.transform = flutterStyle.transform; } // Position (for Positioned widget) if (flutterStyle.position === 'absolute') { properties.positioned = { top: flutterStyle.top, bottom: flutterStyle.bottom, left: flutterStyle.left, right: flutterStyle.right, }; } return properties; } /** * Build BoxDecoration string * @param {Object} decorationParts - Decoration parts * @returns {string} Flutter BoxDecoration expression * @private */ _buildBoxDecoration(decorationParts) { const parts = []; if (decorationParts.color) { parts.push(`color: ${decorationParts.color}`); } if (decorationParts.borderRadius) { parts.push(`borderRadius: ${decorationParts.borderRadius}`); } // Handle border - if we have borderColor but no border, or border without color, combine them if (decorationParts.border) { // If border exists but borderColor is also set, update the border if (decorationParts.borderColor && !decorationParts.border.includes('color:')) { // Extract width from existing border const widthMatch = decorationParts.border.match(/width:\s*(\d+(?:\.\d+)?)/); const width = widthMatch ? widthMatch[1] : '1'; parts.push(`border: Border.all(width: ${width}, color: ${decorationParts.borderColor})`); } else { parts.push(`border: ${decorationParts.border}`); } } else if (decorationParts.borderColor) { parts.push(`border: Border.all(color: ${decorationParts.borderColor})`); } if (decorationParts.boxShadow) { parts.push(`boxShadow: ${decorationParts.boxShadow}`); } if (parts.length === 0) return null; return `BoxDecoration(${parts.join(', ')})`; } /** * Build BoxConstraints string * @param {Object} constraints - Constraints object * @returns {string} Flutter BoxConstraints expression * @private */ _buildConstraints(constraints) { const parts = []; if (constraints.minWidth !== undefined) { parts.push(`minWidth: ${constraints.minWidth}`); } if (constraints.maxWidth !== undefined) { parts.push(`maxWidth: ${constraints.maxWidth}`); } if (constraints.minHeight !== undefined) { parts.push(`minHeight: ${constraints.minHeight}`); } if (constraints.maxHeight !== undefined) { parts.push(`maxHeight: ${constraints.maxHeight}`); } if (parts.length === 0) return null; return `BoxConstraints(${parts.join(', ')})`; } /** * Build TextStyle string * @param {Object} textStyleParts - TextStyle parts * @returns {string} Flutter TextStyle expression * @private */ _buildTextStyle(textStyleParts) { const parts = []; if (textStyleParts.fontSize !== undefined) { parts.push(`fontSize: ${textStyleParts.fontSize}`); } if (textStyleParts.fontWeight) { parts.push(`fontWeight: ${textStyleParts.fontWeight}`); } if (textStyleParts.fontFamily) { parts.push(`fontFamily: ${textStyleParts.fontFamily}`); } if (textStyleParts.color) { parts.push(`color: ${textStyleParts.color}`); } if (textStyleParts.height !== undefined) { parts.push(`height: ${textStyleParts.height}`); } if (textStyleParts.letterSpacing !== undefined) { parts.push(`letterSpacing: ${textStyleParts.letterSpacing}`); } if (textStyleParts.decoration) { parts.push(`decoration: ${textStyleParts.decoration}`); } if (parts.length === 0) return null; return `TextStyle(${parts.join(', ')})`; } /** * Extract numeric value from string * @param {string|number} value - Value to extract from * @returns {number} Extracted number * @private */ _extractNumber(value) { if (typeof value === 'number') return value; const num = parseFloat(value); return isNaN(num) ? 0 : num; } } module.exports = new StyleConverter();