mix_tailwinds 0.0.1-alpha.1 copy "mix_tailwinds: ^0.0.1-alpha.1" to clipboard
mix_tailwinds: ^0.0.1-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_tailwinds/mix_tailwinds.dart';

import 'card_alert_preview.dart';
import 'gradient_debug_preview.dart';

void main() {
  // Ensure debug paint overlays are disabled for parity screenshots.
  debugPaintBaselinesEnabled = false;
  debugPaintSizeEnabled = false;
  debugPaintPointersEnabled = false;
  debugRepaintRainbowEnabled = false;
  runApp(const TailwindParityApp());
}

/// 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`
  /// - `gradient-debug`
  static String get example {
    if (!kIsWeb) return 'dashboard';
    final params = Uri.base.queryParameters;
    return params['example'] ?? 'dashboard';
  }

  /// 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 = TwConfig.standard().copyWith(
      gradientStrategy: ScreenshotConfig.gradientStrategy,
    );

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

      if (example == 'gradient-debug') {
        return TwScope(
          config: twConfig,
          child: MaterialApp(
            debugShowCheckedModeBanner: false,
            home: Scaffold(
              backgroundColor: const Color(0xFFE2E8F0),
              body: SingleChildScrollView(
                child: GradientDebugPreview(width: width),
              ),
            ),
          ),
        );
      }

      // Card alert example - use slate-900 background to match gradient edge
      if (example == 'card-alert') {
        const slate900 = Color(0xFF0F172A);
        return TwScope(
          config: twConfig,
          child: MaterialApp(
            debugShowCheckedModeBanner: false,
            theme: ThemeData(
              scaffoldBackgroundColor: slate900,
              canvasColor: slate900,
            ),
            home: const Scaffold(
              backgroundColor: slate900,
              body: CardAlertPreview(),
            ),
          ),
        );
      }

      // Default dashboard example
      return TwScope(
        config: twConfig,
        child: MaterialApp(
          debugShowCheckedModeBanner: false,
          home: Scaffold(
            backgroundColor: const Color(0xFFF3F4F6),
            body: SingleChildScrollView(
              child: TailwindParityPreview(width: width, scrollable: false),
            ),
          ),
        ),
      );
    }

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

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) {
    return Scaffold(
      backgroundColor: const Color(0xFFF3F4F6),
      body: SafeArea(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const _Header(),
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 24),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  P(
                    text:
                        'Preview width: ${_previewWidth.toStringAsFixed(0)} px',
                    classNames: 'text-base font-semibold text-gray-700',
                  ),
                  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_tailwinds 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(
      classNames: 'flex flex-col gap-6 w-full',
      children: const [_CampaignOverviewCard(), _TeamActivityCard()],
    );
  }
}

class _CampaignOverviewCard extends StatelessWidget {
  const _CampaignOverviewCard();

  @override
  Widget build(BuildContext context) {
    return Div(
      classNames:
          'flex flex-col gap-5 rounded-2xl border border-gray-200 bg-white p-6 shadow-lg',
      children: [
        const P(
          text: 'Campaign Health',
          classNames: 'text-sm font-semibold uppercase text-blue-700',
        ),
        const H1(
          text: 'November brand push',
          classNames: 'text-3xl font-semibold text-gray-700',
        ),
        const P(
          text:
              'Live performance snapshot for paid, lifecycle, and organic channels.',
          classNames: 'text-base text-gray-500',
        ),
        Div(
          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(
          classNames: 'flex flex-col gap-3 md:flex-row',
          children: [
            Div(
              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',
              child: const Span(text: 'View live dashboard'),
            ),
            Div(
              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',
              child: const Span(text: '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(
      classNames: 'flex flex-1 flex-col gap-2 rounded-xl bg-blue-50 p-4',
      children: [
        P(
          text: label,
          classNames: 'text-sm font-semibold uppercase text-blue-700',
        ),
        P(text: value, classNames: 'text-2xl font-semibold text-gray-700'),
        P(text: change, classNames: 'text-sm text-blue-700'),
      ],
    );
  }
}

class _TeamActivityCard extends StatelessWidget {
  const _TeamActivityCard();

  @override
  Widget build(BuildContext context) {
    return Div(
      classNames:
          'flex flex-col gap-5 rounded-2xl border border-gray-200 bg-white p-6 shadow-lg',
      children: [
        const P(
          text: 'Team activity',
          classNames: 'text-sm font-semibold uppercase text-blue-700',
        ),
        const H2(
          text: 'Channel owners',
          classNames: 'text-2xl font-semibold text-gray-700',
        ),
        const P(
          text: 'Latest updates from lifecycle, paid, and organic squads.',
          classNames: 'text-base text-gray-500',
        ),
        Div(
          classNames: 'flex flex-col',
          children: 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(
      classNames: 'flex items-center justify-between gap-4 py-4 $borderClass',
      children: [
        Div(
          classNames: 'flex flex-1 items-center gap-4',
          children: [
            Div(
              classNames:
                  'flex h-12 w-12 items-center justify-center rounded-full bg-gray-100',
              child: Span(
                text: avatarLetter,
                classNames: 'text-lg font-semibold text-gray-700',
              ),
            ),
            Div(
              classNames: 'flex flex-1 flex-col gap-1',
              children: [
                P(
                  text: name,
                  classNames: 'text-base font-semibold text-gray-700',
                ),
                P(text: role, classNames: 'text-sm text-gray-500'),
                Div(
                  classNames: 'flex items-center gap-2',
                  children: [
                    Div(classNames: 'h-1 w-1 rounded-full $badgeColor'),
                    P(text: update, classNames: 'text-sm text-gray-500'),
                  ],
                ),
              ],
            ),
          ],
        ),
        P(text: timeago, classNames: 'text-sm text-gray-500'),
      ],
    );
  }
}
0
likes
70
points
104
downloads

Publisher

unverified uploader

Weekly Downloads

Tailwind-like class utilities mapped to Mix 2.0 stylers.

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

flutter, mix

More

Packages that depend on mix_tailwinds