skills/arustydev/ai/design-to-flutter

design-to-flutter

SKILL.md

Design to Flutter

Convert design component specifications to Flutter widgets.

Overview

This skill transforms design specifications into idiomatic Flutter widgets with proper theming, composition patterns, and Material/Cupertino compliance.

This skill covers:

  • Converting design frames to Flutter widget trees
  • Mapping design properties to widget parameters
  • Generating theme-aware widgets
  • Creating widget tests

This skill does NOT cover:

  • Design token extraction (see design-tokens-extraction skill)
  • State management (Provider, Riverpod, Bloc)
  • Navigation and routing

Quick Reference

Design to Flutter Mapping

Design Element Flutter Widget
Frame/Group Container, SizedBox
Auto-layout horizontal Row
Auto-layout vertical Column
Overlapping layers Stack
Text Text
Rectangle Container with BoxDecoration
Circle Container with BoxShape.circle
Image Image.asset / Image.network
Scroll SingleChildScrollView, ListView
Grid GridView

Layout Properties

Design Property Flutter Equivalent
Padding Padding widget or padding parameter
Gap/Spacing SizedBox or MainAxisAlignment.spaceBetween
Alignment MainAxisAlignment, CrossAxisAlignment
Fill container Expanded, Flexible
Fixed size SizedBox, Container with constraints

Workflow: Convert Design Component

Step 1: Analyze Design Structure

Read the design component and identify:

  • Widget hierarchy and nesting
  • Layout direction and spacing
  • Theme token usage
  • Interactive elements

Step 2: Generate Widget Code

import 'package:flutter/material.dart';
import '../theme/app_theme.dart';

class ComponentName extends StatelessWidget {
  final String title;
  final String? subtitle;
  final VoidCallback? onTap;

  const ComponentName({
    super.key,
    required this.title,
    this.subtitle,
    this.onTap,
  });

  
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return GestureDetector(
      onTap: onTap,
      child: Container(
        padding: EdgeInsets.all(AppTheme.spacing.md),
        decoration: BoxDecoration(
          color: theme.colorScheme.surface,
          borderRadius: BorderRadius.circular(AppTheme.radius.md),
          boxShadow: [AppTheme.shadows.sm],
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              title,
              style: theme.textTheme.bodyLarge,
            ),
            if (subtitle != null) ...[
              SizedBox(height: AppTheme.spacing.xs),
              Text(
                subtitle!,
                style: theme.textTheme.bodySmall?.copyWith(
                  color: theme.colorScheme.onSurfaceVariant,
                ),
              ),
            ],
          ],
        ),
      ),
    );
  }
}

Step 3: Generate Theme Extensions

import 'package:flutter/material.dart';

class AppTheme {
  // Spacing scale
  static const spacing = _Spacing();

  // Border radius
  static const radius = _Radius();

  // Shadows
  static const shadows = _Shadows();

  // Light theme
  static ThemeData get light => ThemeData(
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(
      seedColor: const Color(0xFF2196F3),
      brightness: Brightness.light,
    ),
    textTheme: _textTheme,
  );

  // Dark theme
  static ThemeData get dark => ThemeData(
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(
      seedColor: const Color(0xFF2196F3),
      brightness: Brightness.dark,
    ),
    textTheme: _textTheme,
  );

  static const _textTheme = TextTheme(
    displayLarge: TextStyle(fontSize: 57, fontWeight: FontWeight.w400),
    displayMedium: TextStyle(fontSize: 45, fontWeight: FontWeight.w400),
    headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.w600),
    headlineMedium: TextStyle(fontSize: 28, fontWeight: FontWeight.w600),
    bodyLarge: TextStyle(fontSize: 16, fontWeight: FontWeight.w400),
    bodyMedium: TextStyle(fontSize: 14, fontWeight: FontWeight.w400),
    labelLarge: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
  );
}

class _Spacing {
  const _Spacing();

  double get xs => 4;
  double get sm => 8;
  double get md => 16;
  double get lg => 24;
  double get xl => 32;
}

class _Radius {
  const _Radius();

  double get sm => 4;
  double get md => 8;
  double get lg => 16;
  double get full => 9999;
}

class _Shadows {
  const _Shadows();

  BoxShadow get sm => BoxShadow(
    color: Colors.black.withOpacity(0.05),
    blurRadius: 2,
    offset: const Offset(0, 1),
  );

  BoxShadow get md => BoxShadow(
    color: Colors.black.withOpacity(0.1),
    blurRadius: 6,
    offset: const Offset(0, 4),
  );

  BoxShadow get lg => BoxShadow(
    color: Colors.black.withOpacity(0.1),
    blurRadius: 15,
    offset: const Offset(0, 10),
  );
}

Common Patterns

Card Widget

class AppCard extends StatelessWidget {
  final Widget child;
  final EdgeInsetsGeometry? padding;
  final VoidCallback? onTap;

  const AppCard({
    super.key,
    required this.child,
    this.padding,
    this.onTap,
  });

  
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    Widget card = Container(
      padding: padding ?? EdgeInsets.all(AppTheme.spacing.md),
      decoration: BoxDecoration(
        color: theme.colorScheme.surface,
        borderRadius: BorderRadius.circular(AppTheme.radius.lg),
        boxShadow: [AppTheme.shadows.md],
      ),
      child: child,
    );

    if (onTap != null) {
      return InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(AppTheme.radius.lg),
        child: card,
      );
    }

    return card;
  }
}

Button Widget

enum ButtonVariant { primary, secondary, ghost }
enum ButtonSize { small, medium, large }

class AppButton extends StatelessWidget {
  final String label;
  final VoidCallback? onPressed;
  final ButtonVariant variant;
  final ButtonSize size;
  final IconData? leftIcon;
  final IconData? rightIcon;
  final bool isLoading;

  const AppButton({
    super.key,
    required this.label,
    this.onPressed,
    this.variant = ButtonVariant.primary,
    this.size = ButtonSize.medium,
    this.leftIcon,
    this.rightIcon,
    this.isLoading = false,
  });

  
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    final padding = switch (size) {
      ButtonSize.small => EdgeInsets.symmetric(horizontal: 12, vertical: 8),
      ButtonSize.medium => EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      ButtonSize.large => EdgeInsets.symmetric(horizontal: 24, vertical: 16),
    };

    final textStyle = switch (size) {
      ButtonSize.small => theme.textTheme.labelSmall,
      ButtonSize.medium => theme.textTheme.labelLarge,
      ButtonSize.large => theme.textTheme.labelLarge?.copyWith(fontSize: 16),
    };

    final (backgroundColor, foregroundColor) = switch (variant) {
      ButtonVariant.primary => (
        theme.colorScheme.primary,
        theme.colorScheme.onPrimary,
      ),
      ButtonVariant.secondary => (
        theme.colorScheme.secondaryContainer,
        theme.colorScheme.onSecondaryContainer,
      ),
      ButtonVariant.ghost => (
        Colors.transparent,
        theme.colorScheme.primary,
      ),
    };

    return Material(
      color: backgroundColor,
      borderRadius: BorderRadius.circular(AppTheme.radius.md),
      child: InkWell(
        onTap: isLoading ? null : onPressed,
        borderRadius: BorderRadius.circular(AppTheme.radius.md),
        child: Padding(
          padding: padding,
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              if (isLoading)
                SizedBox(
                  width: 16,
                  height: 16,
                  child: CircularProgressIndicator(
                    strokeWidth: 2,
                    color: foregroundColor,
                  ),
                )
              else if (leftIcon != null)
                Icon(leftIcon, size: 20, color: foregroundColor),
              if (leftIcon != null || isLoading) SizedBox(width: 8),
              Text(label, style: textStyle?.copyWith(color: foregroundColor)),
              if (rightIcon != null) ...[
                SizedBox(width: 8),
                Icon(rightIcon, size: 20, color: foregroundColor),
              ],
            ],
          ),
        ),
      ),
    );
  }
}

List Item Widget

class ListItem extends StatelessWidget {
  final String title;
  final String? subtitle;
  final Widget? leading;
  final Widget? trailing;
  final VoidCallback? onTap;

  const ListItem({
    super.key,
    required this.title,
    this.subtitle,
    this.leading,
    this.trailing,
    this.onTap,
  });

  
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return InkWell(
      onTap: onTap,
      child: Padding(
        padding: EdgeInsets.symmetric(
          horizontal: AppTheme.spacing.md,
          vertical: AppTheme.spacing.sm,
        ),
        child: Row(
          children: [
            if (leading != null) ...[
              leading!,
              SizedBox(width: AppTheme.spacing.md),
            ],
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(title, style: theme.textTheme.bodyLarge),
                  if (subtitle != null)
                    Text(
                      subtitle!,
                      style: theme.textTheme.bodySmall?.copyWith(
                        color: theme.colorScheme.onSurfaceVariant,
                      ),
                    ),
                ],
              ),
            ),
            if (trailing != null) ...[
              SizedBox(width: AppTheme.spacing.md),
              trailing!,
            ],
          ],
        ),
      ),
    );
  }
}

Widget Test Template

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:your_app/widgets/component_name.dart';

void main() {
  group('ComponentName', () {
    testWidgets('renders title', (tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Scaffold(
            body: ComponentName(title: 'Test Title'),
          ),
        ),
      );

      expect(find.text('Test Title'), findsOneWidget);
    });

    testWidgets('renders subtitle when provided', (tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Scaffold(
            body: ComponentName(
              title: 'Title',
              subtitle: 'Subtitle',
            ),
          ),
        ),
      );

      expect(find.text('Subtitle'), findsOneWidget);
    });

    testWidgets('calls onTap when tapped', (tester) async {
      var tapped = false;

      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: ComponentName(
              title: 'Title',
              onTap: () => tapped = true,
            ),
          ),
        ),
      );

      await tester.tap(find.byType(ComponentName));
      expect(tapped, isTrue);
    });
  });
}

File Structure

lib/
├── theme/
│   ├── app_theme.dart
│   ├── colors.dart
│   └── typography.dart
├── widgets/
│   ├── app_button.dart
│   ├── app_card.dart
│   ├── list_item.dart
│   └── widgets.dart  // barrel export
└── main.dart

See Also

  • design-tokens-extraction skill - Extract tokens first
  • design-token-json style - Token format reference
  • component-spec style - Documentation format
Weekly Installs
1
Repository
arustydev/ai
GitHub Stars
6
First Seen
7 days ago
Installed on
zencoder1
amp1
cline1
openclaw1
opencode1
cursor1