mix_winds 0.1.0-alpha.1 copy "mix_winds: ^0.1.0-alpha.1" to clipboard
mix_winds: ^0.1.0-alpha.1 copied to clipboard

Tailwind-like class utilities mapped to Mix 2.0 stylers.

example/lib/main.dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:mix_winds/mix_winds.dart';

import 'advanced_parity_preview.dart';
import 'card_alert_preview.dart';
import 'complex_parity_preview.dart';
import 'flowbite_card_preview.dart';
import 'gradient_debug_preview.dart';
import 'showcase/showcase_embedded_example.dart';
import 'showcase/showcase_multi_view_app.dart';

const _screenshotParityFontFamily = 'TwParityRoboto';

void _noop() {}

void main() {
  final binding = WidgetsFlutterBinding.ensureInitialized();

  // Ensure debug paint overlays are disabled for parity screenshots.
  debugPaintBaselinesEnabled = false;
  debugPaintSizeEnabled = false;
  debugPaintPointersEnabled = false;
  debugRepaintRainbowEnabled = false;

  // Embedded Flutter web runs without an implicit view. The JavaScript host
  // attaches one or more views after the engine starts and selects the example
  // for each view through initialData.
  if (kIsWeb && binding.platformDispatcher.implicitView == null) {
    runWidget(
      const ShowcaseMultiViewApp(viewBuilder: _buildShowcaseEmbeddedExample),
    );
    return;
  }

  runApp(const TailwindParityApp());
}

Widget _buildShowcaseEmbeddedExample(BuildContext context) {
  return const ShowcaseEmbeddedExample();
}

/// Parses URL query parameters for screenshot mode (web only).
/// Usage: ?screenshot=true&width=480&example=card-alert&gradient=css-angle-rect
class ScreenshotConfig {
  static bool get isScreenshotMode {
    if (!kIsWeb) return false;
    final params = Uri.base.queryParameters;
    return params['screenshot'] == 'true';
  }

  static double get width {
    if (!kIsWeb) return 480;
    final params = Uri.base.queryParameters;
    return double.tryParse(params['width'] ?? '') ?? 480;
  }

  /// Returns the example to display.
  ///
  /// Supported values:
  /// - `dashboard` (default)
  /// - `card-alert`
  /// - `complex-parity`
  /// - `flowbite-card`
  /// - `gradient-debug`
  /// - `advanced-parity`
  static String get example {
    if (!kIsWeb) return 'dashboard';
    final params = Uri.base.queryParameters;
    return params['example'] ?? 'dashboard';
  }

  /// Returns the isolated complex visual-parity case (`01` through `10`).
  static String get complexCase {
    if (!kIsWeb) return '01';
    return Uri.base.queryParameters['case'] ?? '01';
  }

  /// Returns the isolated advanced visual-parity example (`01` through `05`).
  static String get advancedExample {
    if (!kIsWeb) return '01';
    return Uri.base.queryParameters['sample'] ?? '01';
  }

  /// Returns gradient strategy for screenshot experiments.
  ///
  /// Accepted values:
  /// - css-angle-rect (default)
  /// - alignment
  /// - angle
  static TwGradientStrategy get gradientStrategy {
    if (!kIsWeb) return TwGradientStrategy.cssAngleRect;
    final params = Uri.base.queryParameters;
    final raw = params['gradient'];
    if (raw == 'angle') return TwGradientStrategy.angle;
    if (raw == 'css-angle-rect' || raw == 'cssAngleRect' || raw == 'adaptive') {
      return TwGradientStrategy.cssAngleRect;
    }
    if (raw == 'alignment') return TwGradientStrategy.alignment;
    return TwGradientStrategy.cssAngleRect;
  }
}

class TailwindParityApp extends StatelessWidget {
  const TailwindParityApp({super.key});

  @override
  Widget build(BuildContext context) {
    final twConfig = _twConfigForCurrentMode();

    // Screenshot mode: render clean preview without UI chrome
    if (ScreenshotConfig.isScreenshotMode) {
      final width = ScreenshotConfig.width;
      final example = ScreenshotConfig.example;

      if (example == 'complex-parity') {
        return _ScreenshotWidgetsApp(
          backgroundColor: const Color(0xFFF3F4F6),
          child: TwScope(
            config: twConfig,
            child: ComplexParityPreview(
              caseId: ScreenshotConfig.complexCase,
              width: width,
            ),
          ),
        );
      }

      if (example == 'gradient-debug') {
        return _ScreenshotWidgetsApp(
          backgroundColor: const Color(0xFFE2E8F0),
          child: TwScope(
            config: twConfig,
            child: SingleChildScrollView(
              child: GradientDebugPreview(width: width),
            ),
          ),
        );
      }

      if (example == 'advanced-parity') {
        const slate100 = Color(0xFFF1F5F9);
        return _ScreenshotWidgetsApp(
          backgroundColor: slate100,
          child: TwScope(
            config: twConfig,
            child: SingleChildScrollView(
              child: AdvancedParityPreview(
                exampleId: ScreenshotConfig.advancedExample,
              ),
            ),
          ),
        );
      }

      // Card alert example - use slate-900 background to match gradient edge
      if (example == 'card-alert') {
        const slate900 = Color(0xFF0F172A);
        return _ScreenshotWidgetsApp(
          backgroundColor: slate900,
          child: TwScope(config: twConfig, child: const CardAlertPreview()),
        );
      }

      if (example == 'flowbite-card') {
        const gray50 = Color(0xFFF9FAFB);
        return _ScreenshotWidgetsApp(
          backgroundColor: gray50,
          child: TwScope(
            config: flowbiteCardTwConfig(twConfig),
            child: const SingleChildScrollView(
              child: Padding(
                padding: EdgeInsets.all(16),
                child: Align(
                  alignment: Alignment.topCenter,
                  child: FlowbiteCardPreview(),
                ),
              ),
            ),
          ),
        );
      }

      // Default dashboard example
      return _ScreenshotWidgetsApp(
        backgroundColor: const Color(0xFFF3F4F6),
        child: TwScope(
          config: twConfig,
          child: SingleChildScrollView(
            child: Padding(
              padding: const EdgeInsets.symmetric(vertical: 40),
              child: TailwindParityPreview(width: width, scrollable: false),
            ),
          ),
        ),
      );
    }

    return MaterialApp(
      title: 'mix_winds vs Tailwind CSS',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2563EB)),
        useMaterial3: true,
      ),
      home: TwScope(config: twConfig, child: const TailwindParityScreen()),
    );
  }
}

class _ScreenshotWidgetsApp extends StatelessWidget {
  const _ScreenshotWidgetsApp({
    required this.backgroundColor,
    required this.child,
  });

  final Color backgroundColor;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return WidgetsApp(
      color: backgroundColor,
      debugShowCheckedModeBanner: false,
      pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
        return PageRouteBuilder<T>(
          settings: settings,
          pageBuilder: (context, animation, secondaryAnimation) {
            return builder(context);
          },
        );
      },
      home: ColoredBox(
        color: backgroundColor,
        child: SizedBox.expand(child: child),
      ),
    );
  }
}

TwConfig _twConfigForCurrentMode() {
  final baseConfig = TwConfig.standard().copyWith(
    gradientStrategy: ScreenshotConfig.gradientStrategy,
  );

  if (!ScreenshotConfig.isScreenshotMode) return baseConfig;

  return baseConfig.copyWith(
    textDefaults: baseConfig.textDefaults.copyWith(
      fontFamily: _screenshotParityFontFamily,
      fontFamilyFallback: const [],
    ),
  );
}

class TailwindParityScreen extends StatefulWidget {
  const TailwindParityScreen({super.key, this.initialWidth = 420});

  final double initialWidth;

  @override
  State<TailwindParityScreen> createState() => _TailwindParityScreenState();
}

class _TailwindParityScreenState extends State<TailwindParityScreen> {
  late double _previewWidth;

  @override
  void initState() {
    super.initState();
    _previewWidth = widget.initialWidth;
  }

  void _updateWidth(double value) {
    setState(() {
      _previewWidth = value;
    });
  }

  @override
  Widget build(BuildContext context) {
    final twConfig = TwConfigProvider.of(context);

    return Scaffold(
      backgroundColor: const Color(0xFFF3F4F6),
      body: TwScope(
        config: twConfig,
        child: SafeArea(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              const _Header(),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 24),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    p(
                      'text-base font-semibold text-gray-700',
                      'Preview width: ${_previewWidth.toStringAsFixed(0)} px',
                    ),
                    Slider(
                      min: 320,
                      max: 1040,
                      divisions: 9,
                      label: _previewWidth.toStringAsFixed(0),
                      value: _previewWidth,
                      onChanged: _updateWidth,
                    ),
                  ],
                ),
              ),
              const SizedBox(height: 12),
              Expanded(
                child: Center(
                  child: TailwindParityPreview(width: _previewWidth),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _Header extends StatelessWidget {
  const _Header();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(24, 24, 24, 12),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: const [
          Text(
            'mix_winds parity samples',
            style: TextStyle(fontSize: 28, fontWeight: FontWeight.w700),
          ),
          SizedBox(height: 8),
          Text(
            'Use the slider to exercise the same responsive Tailwind tokens the web sample uses.',
          ),
        ],
      ),
    );
  }
}

class _ComparisonStack extends StatelessWidget {
  const _ComparisonStack();

  @override
  Widget build(BuildContext context) {
    return div('flex w-full flex-col gap-6', const [
      _CampaignOverviewCard(),
      _TeamActivityCard(),
    ]);
  }
}

class _CampaignOverviewCard extends StatelessWidget {
  const _CampaignOverviewCard();

  @override
  Widget build(BuildContext context) {
    return div(
      'flex flex-col gap-5 rounded-2xl border border-gray-200 bg-white p-6 shadow-lg',
      [
        p('text-sm font-semibold uppercase text-blue-700', 'Campaign Health'),
        h1('text-3xl font-semibold text-gray-700', 'November brand push'),
        p(
          'text-base text-gray-500',
          'Live performance snapshot for paid, lifecycle, and organic channels.',
        ),
        Div(
          key: const ValueKey('dashboard-metric-row'),
          classNames:
              'flex flex-col gap-4 border-t border-gray-200 pt-4 md:flex-row',
          children: const [
            _MetricTile(
              label: 'Spend',
              value: r'$241.18M',
              change: '+8.6% vs last week',
            ),
            _MetricTile(label: 'Return', value: '4.8x', change: '+0.4 uplift'),
            _MetricTile(
              label: 'CPA',
              value: r'$248.30',
              change: '-12% efficiency gain',
            ),
          ],
        ),
        Div(
          key: const ValueKey('dashboard-button-row'),
          classNames: 'flex flex-col gap-3 md:flex-row',
          children: [
            Button(
              key: const ValueKey('dashboard-view-button'),
              classNames:
                  'flex flex-1 items-center justify-center rounded-full bg-blue-600 px-4 py-3 text-base font-semibold text-white hover:bg-blue-700',
              onPressed: _noop,
              child: span('', 'View live dashboard'),
            ),
            Button(
              key: const ValueKey('dashboard-download-button'),
              classNames:
                  'flex flex-1 items-center justify-center rounded-full border border-blue-600 px-4 py-3 text-base font-semibold text-blue-600 hover:bg-blue-50',
              onPressed: _noop,
              child: span('', 'Download CSV'),
            ),
          ],
        ),
      ],
    );
  }
}

class _MetricTile extends StatelessWidget {
  const _MetricTile({
    required this.label,
    required this.value,
    required this.change,
  });

  final String label;
  final String value;
  final String change;

  @override
  Widget build(BuildContext context) {
    return Div(
      key: ValueKey('dashboard-metric-$label'),
      classNames: 'flex flex-1 flex-col gap-2 rounded-xl bg-blue-50 p-4',
      children: [
        p('text-sm font-semibold uppercase text-blue-700', label),
        p('text-2xl font-semibold text-gray-700', value),
        p('text-sm text-blue-700', change),
      ],
    );
  }
}

class _TeamActivityCard extends StatelessWidget {
  const _TeamActivityCard();

  @override
  Widget build(BuildContext context) {
    return div(
      'flex flex-col gap-5 rounded-2xl border border-gray-200 bg-white p-6 shadow-lg',
      [
        p('text-sm font-semibold uppercase text-blue-700', 'Team activity'),
        h2('text-2xl font-semibold text-gray-700', 'Channel owners'),
        p(
          'text-base text-gray-500',
          'Latest updates from lifecycle, paid, and organic squads.',
        ),
        div('flex flex-col', const [
          _ActivityRow(
            name: 'Rita Carr',
            role: 'Lifecycle · Email',
            update: 'Shipped reactivation flow revamp',
            timeago: '12m ago',
            accentIndex: 0,
            showBorder: false,
          ),
          _ActivityRow(
            name: 'Jalen Ruiz',
            role: 'Paid · Social',
            update: 'Cut CPA by 14% on TikTok lookalikes',
            timeago: '1h ago',
            accentIndex: 1,
          ),
          _ActivityRow(
            name: 'Mara Singh',
            role: 'Organic · Web',
            update: 'Published performance teardown for Q4',
            timeago: '3h ago',
            accentIndex: 2,
          ),
        ]),
      ],
    );
  }
}

class TailwindParityPreview extends StatelessWidget {
  const TailwindParityPreview({
    super.key,
    required this.width,
    this.scrollable = true,
  });

  final double width;
  final bool scrollable;

  @override
  Widget build(BuildContext context) {
    final content = SizedBox(width: width, child: const _ComparisonStack());

    if (!scrollable) {
      return Align(
        alignment: Alignment.topCenter,
        heightFactor: 1,
        widthFactor: 1,
        child: content,
      );
    }

    return Align(
      alignment: Alignment.topCenter,
      child: SingleChildScrollView(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
        child: content,
      ),
    );
  }
}

class _ActivityRow extends StatelessWidget {
  const _ActivityRow({
    required this.name,
    required this.role,
    required this.update,
    required this.timeago,
    required this.accentIndex,
    this.showBorder = true,
  });

  final String name;
  final String role;
  final String update;
  final String timeago;
  final int accentIndex;
  final bool showBorder;

  @override
  Widget build(BuildContext context) {
    final borderClass = showBorder ? 'border-t border-gray-100' : '';
    final avatarLetter = name.isNotEmpty ? name[0] : '?';
    final badgeColor = switch (accentIndex % 3) {
      0 => 'bg-blue-100',
      1 => 'bg-blue-50',
      _ => 'bg-gray-200',
    };

    return Div(
      key: ValueKey('dashboard-activity-$name'),
      classNames: 'flex items-center justify-between gap-4 $borderClass py-4',
      children: [
        div('flex flex-1 items-center gap-4', [
          Div(
            classNames:
                'flex h-12 w-12 items-center justify-center rounded-full bg-gray-100',
            child: span('text-lg font-semibold text-gray-700', avatarLetter),
          ),
          div('flex flex-1 flex-col gap-1', [
            p('text-base font-semibold text-gray-700', name),
            p('text-sm text-gray-500', role),
            div('flex items-center gap-2', [
              div('h-1 w-1 rounded-full $badgeColor'),
              p('text-sm text-gray-500', update),
            ]),
          ]),
        ]),
        p('text-sm text-gray-500', timeago),
      ],
    );
  }
}
0
likes
160
points
28
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Tailwind-like class utilities mapped to Mix 2.0 stylers.

Homepage
Repository (GitHub)
View/report issues

License

BSD-3-Clause (license)

Dependencies

flutter, mix

More

Packages that depend on mix_winds