advance_cart_stepper 0.0.1 copy "advance_cart_stepper: ^0.0.1" to clipboard
advance_cart_stepper: ^0.0.1 copied to clipboard

A customizable expandable cart quantity stepper widget for Flutter with async support, loading indicators, and theming.

example/lib/main.dart

import 'package:advance_cart_stepper/advance_cart_stepper.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const CartStepperExampleApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Cart Stepper Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFFD84315)),
        useMaterial3: true,
      ),
      darkTheme: ThemeData.dark(useMaterial3: true).copyWith(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFFD84315),
          brightness: Brightness.dark,
        ),
      ),
      home: const CartStepperDemo(),
    );
  }
}

class CartStepperDemo extends StatefulWidget {
  const CartStepperDemo({super.key});

  @override
  State<CartStepperDemo> createState() => _CartStepperDemoState();
}

class _CartStepperDemoState extends State<CartStepperDemo>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 6, vsync: this);
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Cart Stepper Demo'),
        bottom: TabBar(
          controller: _tabController,
          isScrollable: true,
          tabAlignment: TabAlignment.start,
          tabs: const [
            Tab(text: 'Basics'),
            Tab(text: 'Styles'),
            Tab(text: 'Async'),
            Tab(text: 'Advanced'),
            Tab(text: 'Components'),
            Tab(text: 'Real World'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: const [
          _BasicsTab(),
          _StylesTab(),
          _AsyncTab(),
          _AdvancedTab(),
          _ComponentsTab(),
          _RealWorldTab(),
        ],
      ),
    );
  }
}

// =============================================================================
// TAB 1: BASICS
// =============================================================================
class _BasicsTab extends StatefulWidget {
  const _BasicsTab();

  @override
  State<_BasicsTab> createState() => _BasicsTabState();
}

class _BasicsTabState extends State<_BasicsTab>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  // Basic quantities
  int _basicQuantity = 0;
  int _compactQuantity = 0;
  int _normalQuantity = 2;
  int _largeQuantity = 1;

  // Add to cart button styles
  int _circleIconQty = 0;
  int _addButtonQty = 0;
  int _addToCartQty = 0;
  int _iconOnlyQty = 0;
  int _customButtonQty = 0;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        _buildSection(
          context,
          'Size Variants',
          'Different sizes for various use cases',
          [
            _buildRow(
              'Compact (32px)',
              CartStepper(
                quantity: _compactQuantity,
                size: CartStepperSize.compact,
                onQuantityChanged: (qty) =>
                    setState(() => _compactQuantity = qty),
                onRemove: () => setState(() => _compactQuantity = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Normal (40px) - Default',
              CartStepper(
                quantity: _normalQuantity,
                size: CartStepperSize.normal,
                onQuantityChanged: (qty) =>
                    setState(() => _normalQuantity = qty),
                onRemove: () => setState(() => _normalQuantity = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Large (48px)',
              CartStepper(
                quantity: _largeQuantity,
                size: CartStepperSize.large,
                onQuantityChanged: (qty) =>
                    setState(() => _largeQuantity = qty),
                onRemove: () => setState(() => _largeQuantity = 0),
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'Add to Cart Button Styles',
          'Choose between circle icon or button styles',
          [
            _buildRow(
              'Circle Icon (default)',
              CartStepper(
                quantity: _circleIconQty,
                addToCartConfig: AddToCartButtonConfig.circleIcon,
                onQuantityChanged: (qty) =>
                    setState(() => _circleIconQty = qty),
                onRemove: () => setState(() => _circleIconQty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              '"Add" Button',
              CartStepper(
                quantity: _addButtonQty,
                addToCartConfig: AddToCartButtonConfig.addButton,
                onQuantityChanged: (qty) =>
                    setState(() => _addButtonQty = qty),
                onRemove: () => setState(() => _addButtonQty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              '"Add to Cart" Button',
              CartStepper(
                quantity: _addToCartQty,
                addToCartConfig: AddToCartButtonConfig.addToCartButton,
                onQuantityChanged: (qty) =>
                    setState(() => _addToCartQty = qty),
                onRemove: () => setState(() => _addToCartQty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Icon Only Button',
              CartStepper(
                quantity: _iconOnlyQty,
                addToCartConfig: AddToCartButtonConfig.iconOnlyButton,
                onQuantityChanged: (qty) =>
                    setState(() => _iconOnlyQty = qty),
                onRemove: () => setState(() => _iconOnlyQty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Custom Button',
              CartStepper(
                quantity: _customButtonQty,
                addToCartConfig: const AddToCartButtonConfig(
                  style: AddToCartButtonStyle.button,
                  buttonText: 'Buy Now',
                  icon: Icons.shopping_bag,
                  iconLeading: false,
                  buttonWidth: 110,
                  borderRadius: BorderRadius.all(Radius.circular(8)),
                ),
                style: const CartStepperStyle(
                  backgroundColor: Color(0xFF2E7D32),
                  foregroundColor: Colors.white,
                  borderColor: Color(0xFF2E7D32),
                ),
                onQuantityChanged: (qty) =>
                    setState(() => _customButtonQty = qty),
                onRemove: () => setState(() => _customButtonQty = 0),
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'Basic Functionality',
          'Core features: add, increment, decrement, delete',
          [
            _buildRow(
              'Start from 0',
              CartStepper(
                quantity: _basicQuantity,
                onQuantityChanged: (qty) =>
                    setState(() => _basicQuantity = qty),
                onRemove: () => setState(() => _basicQuantity = 0),
              ),
            ),
            const SizedBox(height: 12),
            Text(
              'Tap + to add, use -/+ to adjust, trash to remove',
              style: TextStyle(
                fontSize: 12,
                color: Colors.grey[600],
                fontStyle: FontStyle.italic,
              ),
            ),
          ],
        ),
        const SizedBox(height: 60),
      ],
    );
  }
}

// =============================================================================
// TAB 2: STYLES
// =============================================================================
class _StylesTab extends StatefulWidget {
  const _StylesTab();

  @override
  State<_StylesTab> createState() => _StylesTabState();
}

class _StylesTabState extends State<_StylesTab>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  int _orangeQty = 2;
  int _darkQty = 3;
  int _lightQty = 1;
  int _blueQty = 2;
  int _greenQty = 1;
  int _tealQty = 2;
  int _indigoQty = 3;
  int _animFastQty = 1;
  int _animSmoothQty = 2;
  int _animCustomQty = 1;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        _buildSection(
          context,
          'Pre-built Themes',
          'Ready-to-use style variants',
          [
            _buildRow(
              'Orange (Default)',
              CartStepper(
                quantity: _orangeQty,
                style: CartStepperStyle.defaultOrange,
                onQuantityChanged: (qty) => setState(() => _orangeQty = qty),
                onRemove: () => setState(() => _orangeQty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Dark',
              CartStepper(
                quantity: _darkQty,
                style: CartStepperStyle.dark,
                onQuantityChanged: (qty) => setState(() => _darkQty = qty),
                onRemove: () => setState(() => _darkQty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Light',
              CartStepper(
                quantity: _lightQty,
                style: CartStepperStyle.light,
                onQuantityChanged: (qty) => setState(() => _lightQty = qty),
                onRemove: () => setState(() => _lightQty = 0),
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'Custom Styles',
          'Create your own brand colors',
          [
            _buildRow(
              'Blue with elevation',
              CartStepper(
                quantity: _blueQty,
                style: const CartStepperStyle(
                  backgroundColor: Color(0xFF1976D2),
                  foregroundColor: Colors.white,
                  borderColor: Color(0xFF1976D2),
                  elevation: 4.0,
                  iconScale: 1.1,
                ),
                onQuantityChanged: (qty) => setState(() => _blueQty = qty),
                onRemove: () => setState(() => _blueQty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Green bold font',
              CartStepper(
                quantity: _greenQty,
                style: const CartStepperStyle(
                  backgroundColor: Color(0xFF2E7D32),
                  foregroundColor: Colors.white,
                  borderColor: Color(0xFF2E7D32),
                  fontWeight: FontWeight.w800,
                ),
                onQuantityChanged: (qty) => setState(() => _greenQty = qty),
                onRemove: () => setState(() => _greenQty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Rounded square',
              CartStepper(
                quantity: _tealQty,
                style: CartStepperStyle(
                  borderRadius: BorderRadius.circular(8),
                  backgroundColor: Colors.teal,
                  foregroundColor: Colors.white,
                  borderColor: Colors.teal,
                ),
                onQuantityChanged: (qty) => setState(() => _tealQty = qty),
                onRemove: () => setState(() => _tealQty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Custom font style',
              CartStepper(
                quantity: _indigoQty,
                style: const CartStepperStyle(
                  backgroundColor: Colors.indigo,
                  foregroundColor: Colors.white,
                  borderColor: Colors.indigo,
                  textStyle: TextStyle(
                    fontFamily: 'Courier',
                    fontWeight: FontWeight.bold,
                    letterSpacing: 2,
                  ),
                ),
                onQuantityChanged: (qty) => setState(() => _indigoQty = qty),
                onRemove: () => setState(() => _indigoQty = 0),
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'Animation Presets',
          'Different animation speeds and curves',
          [
            _buildRow(
              'Fast (150ms)',
              CartStepper(
                quantity: _animFastQty,
                animation: CartStepperAnimation.fast,
                onQuantityChanged: (qty) => setState(() => _animFastQty = qty),
                onRemove: () => setState(() => _animFastQty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Smooth (bounce)',
              CartStepper(
                quantity: _animSmoothQty,
                animation: CartStepperAnimation.smooth,
                onQuantityChanged: (qty) =>
                    setState(() => _animSmoothQty = qty),
                onRemove: () => setState(() => _animSmoothQty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Custom (elastic)',
              CartStepper(
                quantity: _animCustomQty,
                animation: const CartStepperAnimation(
                  expandDuration: Duration(milliseconds: 500),
                  expandCurve: Curves.elasticOut,
                  enableHaptics: true,
                ),
                onQuantityChanged: (qty) =>
                    setState(() => _animCustomQty = qty),
                onRemove: () => setState(() => _animCustomQty = 0),
              ),
            ),
          ],
        ),
        const SizedBox(height: 60),
      ],
    );
  }
}

// =============================================================================
// TAB 3: ASYNC
// =============================================================================
class _AsyncTab extends StatefulWidget {
  const _AsyncTab();

  @override
  State<_AsyncTab> createState() => _AsyncTabState();
}

class _AsyncTabState extends State<_AsyncTab>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  int _asyncDefaultQty = 0;
  int _asyncFadingQty = 2;
  int _builtInCircularQty = 1;
  int _builtInLinearQty = 2;
  int _optimisticQty = 3;

  // Debounce mode
  int _debounceQty = 2;
  bool _debounceLoading = false;

  // Error handling
  int _errorQty = 2;
  String? _lastError;
  int _errorBuilderQty = 2;

  // Loading types showcase
  final Map<CartStepperLoadingType, int> _loadingTypeQty = {
    for (var type in CartStepperLoadingType.values) type: 1,
  };

  Future<void> _simulateApiCall({int delayMs = 800}) async {
    await Future.delayed(Duration(milliseconds: delayMs));
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        _buildSection(
          context,
          'Async Loading',
          'Shows loading spinner during API calls',
          [
            _buildRow(
              'ThreeBounce (default)',
              CartStepper(
                quantity: _asyncDefaultQty,
                onQuantityChangedAsync: (qty) async {
                  await _simulateApiCall();
                  setState(() => _asyncDefaultQty = qty);
                },
                onRemoveAsync: () async {
                  await _simulateApiCall();
                  setState(() => _asyncDefaultQty = 0);
                },
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'FadingCircle (slow)',
              CartStepper(
                quantity: _asyncFadingQty,
                loadingConfig: const CartStepperLoadingConfig(
                  type: CartStepperLoadingType.fadingCircle,
                  minimumDuration: Duration(milliseconds: 500),
                ),
                onQuantityChangedAsync: (qty) async {
                  await _simulateApiCall(delayMs: 1200);
                  setState(() => _asyncFadingQty = qty);
                },
                onRemoveAsync: () async {
                  await _simulateApiCall(delayMs: 1200);
                  setState(() => _asyncFadingQty = 0);
                },
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'Built-in Loading (No SpinKit)',
          "Use Flutter's native indicators",
          [
            _buildRow(
              'CircularProgressIndicator',
              CartStepper(
                quantity: _builtInCircularQty,
                loadingConfig: CartStepperLoadingConfig.builtIn,
                onQuantityChangedAsync: (qty) async {
                  await _simulateApiCall();
                  setState(() => _builtInCircularQty = qty);
                },
                onRemoveAsync: () async {
                  await _simulateApiCall();
                  setState(() => _builtInCircularQty = 0);
                },
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'LinearProgressIndicator',
              CartStepper(
                quantity: _builtInLinearQty,
                loadingConfig: const CartStepperLoadingConfig(
                  type: CartStepperLoadingType.linear,
                  sizeMultiplier: 1.2,
                ),
                onQuantityChangedAsync: (qty) async {
                  await _simulateApiCall();
                  setState(() => _builtInLinearQty = qty);
                },
                onRemoveAsync: () async {
                  await _simulateApiCall();
                  setState(() => _builtInLinearQty = 0);
                },
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'Optimistic Updates',
          'Update UI immediately, revert on error',
          [
            _buildRow(
              'Optimistic (instant UI)',
              CartStepper(
                quantity: _optimisticQty,
                optimisticUpdate: true,
                revertOnError: true,
                loadingConfig: const CartStepperLoadingConfig(
                  type: CartStepperLoadingType.pulse,
                  sizeMultiplier: 0.6,
                ),
                onQuantityChangedAsync: (qty) async {
                  await _simulateApiCall(delayMs: 600);
                  setState(() => _optimisticQty = qty);
                },
                onRemoveAsync: () async {
                  await _simulateApiCall(delayMs: 600);
                  setState(() => _optimisticQty = 0);
                },
              ),
            ),
            const SizedBox(height: 8),
            Text(
              'Notice: quantity updates instantly while loading',
              style: TextStyle(
                fontSize: 12,
                color: Colors.grey[600],
                fontStyle: FontStyle.italic,
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'Debounce Mode (Best UX)',
          'Batch rapid changes into one API call',
          [
            _buildRow(
              'Debounce (500ms)',
              Column(
                crossAxisAlignment: CrossAxisAlignment.end,
                children: [
                  CartStepper(
                    quantity: _debounceQty,
                    debounceDelay: const Duration(milliseconds: 500),
                    maxQuantity: 99,
                    loadingConfig: const CartStepperLoadingConfig(
                      type: CartStepperLoadingType.threeBounce,
                    ),
                    onQuantityChangedAsync: (qty) async {
                      setState(() => _debounceLoading = true);
                      await _simulateApiCall(delayMs: 800);
                      setState(() {
                        _debounceQty = qty;
                        _debounceLoading = false;
                      });
                    },
                    onRemoveAsync: () async {
                      setState(() => _debounceLoading = true);
                      await _simulateApiCall(delayMs: 800);
                      setState(() {
                        _debounceQty = 0;
                        _debounceLoading = false;
                      });
                    },
                  ),
                  if (_debounceLoading)
                    const Padding(
                      padding: EdgeInsets.only(top: 4),
                      child: Text(
                        'Syncing...',
                        style: TextStyle(
                          fontSize: 10,
                          color: Colors.orange,
                          fontStyle: FontStyle.italic,
                        ),
                      ),
                    ),
                ],
              ),
            ),
            const SizedBox(height: 12),
            Container(
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: Colors.green.shade50,
                borderRadius: BorderRadius.circular(8),
                border: Border.all(color: Colors.green.shade200),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      Icon(Icons.lightbulb_outline,
                          color: Colors.green.shade700, size: 18),
                      const SizedBox(width: 8),
                      Text(
                        'Try this:',
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          color: Colors.green.shade700,
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 8),
                  Text(
                    '• Tap +/- rapidly multiple times\n'
                    '• Long-press and hold to increment fast\n'
                    '• Notice: UI updates instantly, but only ONE API call is made after you stop!',
                    style: TextStyle(
                      fontSize: 12,
                      color: Colors.green.shade700,
                      height: 1.5,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'Error Handling',
          'Handle async operation errors',
          [
            if (_lastError != null)
              Container(
                padding: const EdgeInsets.all(8),
                margin: const EdgeInsets.only(bottom: 12),
                decoration: BoxDecoration(
                  color: Colors.red.shade50,
                  borderRadius: BorderRadius.circular(8),
                  border: Border.all(color: Colors.red.shade200),
                ),
                child: Row(
                  children: [
                    Icon(Icons.error_outline,
                        color: Colors.red.shade700, size: 20),
                    const SizedBox(width: 8),
                    Expanded(
                      child: Text(
                        _lastError!,
                        style: TextStyle(
                            color: Colors.red.shade700, fontSize: 12),
                      ),
                    ),
                    IconButton(
                      icon: const Icon(Icons.close, size: 16),
                      onPressed: () => setState(() => _lastError = null),
                      padding: EdgeInsets.zero,
                      constraints: const BoxConstraints(),
                    ),
                  ],
                ),
              ),
            _buildRow(
              'With onError callback',
              CartStepper(
                quantity: _errorQty,
                onQuantityChangedAsync: (qty) async {
                  await _simulateApiCall(delayMs: 500);
                  if (qty > 5) {
                    throw Exception('Max 5 items allowed!');
                  }
                  setState(() => _errorQty = qty);
                },
                onError: (error, _) {
                  setState(() => _lastError = error.toString());
                },
                onRemoveAsync: () async {
                  await _simulateApiCall(delayMs: 300);
                  setState(() => _errorQty = 0);
                },
              ),
            ),
            const SizedBox(height: 8),
            Text(
              'Try incrementing past 5 to trigger an error',
              style: TextStyle(
                fontSize: 12,
                color: Colors.grey[600],
                fontStyle: FontStyle.italic,
              ),
            ),
            const SizedBox(height: 20),
            _buildRow(
              'With errorBuilder (inline)',
              CartStepper(
                quantity: _errorBuilderQty,
                onQuantityChangedAsync: (qty) async {
                  await _simulateApiCall(delayMs: 500);
                  if (qty > 4) {
                    throw Exception('Stock limit reached');
                  }
                  setState(() => _errorBuilderQty = qty);
                },
                errorBuilder: (context, error, retry) => Padding(
                  padding: const EdgeInsets.only(top: 8),
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Text(
                        error.message,
                        style:
                            const TextStyle(color: Colors.red, fontSize: 11),
                      ),
                      const SizedBox(width: 4),
                      GestureDetector(
                        onTap: retry,
                        child: const Text(
                          'Retry',
                          style: TextStyle(
                            color: Colors.blue,
                            fontSize: 11,
                            decoration: TextDecoration.underline,
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
                onRemoveAsync: () async {
                  await _simulateApiCall(delayMs: 300);
                  setState(() => _errorBuilderQty = 0);
                },
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'All Loading Types',
          'Tap to see each SpinKit animation',
          [
            Wrap(
              spacing: 8,
              runSpacing: 16,
              children: CartStepperLoadingType.values.map((type) {
                final name = type.name;
                return SizedBox(
                  width: 130,
                  child: Column(
                    children: [
                      Text(
                        name,
                        style: const TextStyle(fontSize: 10),
                        overflow: TextOverflow.ellipsis,
                      ),
                      const SizedBox(height: 4),
                      CartStepper(
                        quantity: _loadingTypeQty[type]!,
                        size: CartStepperSize.compact,
                        loadingConfig: CartStepperLoadingConfig(
                          type: type,
                          sizeMultiplier: 0.7,
                        ),
                        onQuantityChangedAsync: (qty) async {
                          await _simulateApiCall(delayMs: 1500);
                          setState(() => _loadingTypeQty[type] = qty);
                        },
                        onRemoveAsync: () async {
                          await _simulateApiCall(delayMs: 1500);
                          setState(() => _loadingTypeQty[type] = 0);
                        },
                      ),
                    ],
                  ),
                );
              }).toList(),
            ),
          ],
        ),
        const SizedBox(height: 60),
      ],
    );
  }
}

// =============================================================================
// TAB 4: ADVANCED
// =============================================================================
class _AdvancedTab extends StatefulWidget {
  const _AdvancedTab();

  @override
  State<_AdvancedTab> createState() => _AdvancedTabState();
}

class _AdvancedTabState extends State<_AdvancedTab>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  // Min/Max
  int _minMaxQty = 5;
  int _stepQty = 10;
  final int _disabledQty = 3;
  int _noDeleteQty = 1;
  int _deleteViaChangeQty = 1;

  // Long press
  int _fastLongPressQty = 50;
  int _slowLongPressQty = 50;

  // Quantity formatters
  int _abbrev1Qty = 1500;
  int _abbrev2Qty = 12500;
  int _abbrev3Qty = 1000000;
  int _maxIndicatorQty = 99;

  // Callbacks
  int _callbacksQty = 5;
  String? _callbackMessage;

  // Auto-collapse
  int _autoCollapseQty = 2;

  // Initially expanded
  int _expandedTrueQty = 3;
  int _expandedFalseQty = 5;

  // Custom icons
  int _customIconsQty = 2;

  // Validation
  int _validationQty = 2;

  // Manual Input
  int _manualInputQty = 5;
  int _manualInputLargeQty = 25;
  int _customInputBuilderQty = 10;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        _buildSection(
          context,
          'Min/Max & Step',
          'Control quantity limits and step values',
          [
            _buildRow(
              'Min: 5, Max: 15',
              CartStepper(
                quantity: _minMaxQty,
                minQuantity: 5,
                maxQuantity: 15,
                onQuantityChanged: (qty) => setState(() => _minMaxQty = qty),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Step: 5',
              CartStepper(
                quantity: _stepQty,
                minQuantity: 0,
                maxQuantity: 100,
                step: 5,
                quantityFormatter: (q) => '$q items',
                onQuantityChanged: (qty) => setState(() => _stepQty = qty),
                onRemove: () => setState(() => _stepQty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Disabled',
              CartStepper(
                quantity: _disabledQty,
                enabled: false,
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'No delete icon',
              CartStepper(
                quantity: _noDeleteQty,
                showDeleteAtMin: false,
                onQuantityChanged: (qty) => setState(() => _noDeleteQty = qty),
                onRemove: () => setState(() => _noDeleteQty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Delete via onQuantityChanged',
              CartStepper(
                quantity: _deleteViaChangeQty,
                minQuantity: 1,
                deleteViaQuantityChange: true,
                onQuantityChanged: (qty) {
                  if (qty < 1) {
                    setState(() => _deleteViaChangeQty = 0);
                  } else {
                    setState(() => _deleteViaChangeQty = qty);
                  }
                },
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'Long Press Configuration',
          'Control delay before rapid increment starts',
          [
            _buildRow(
              'Fast (100ms delay)',
              CartStepper(
                quantity: _fastLongPressQty,
                maxQuantity: 999,
                initialLongPressDelay: const Duration(milliseconds: 100),
                longPressInterval: const Duration(milliseconds: 50),
                onQuantityChanged: (qty) =>
                    setState(() => _fastLongPressQty = qty),
                onRemove: () => setState(() => _fastLongPressQty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Slow (800ms delay)',
              CartStepper(
                quantity: _slowLongPressQty,
                maxQuantity: 999,
                initialLongPressDelay: const Duration(milliseconds: 800),
                longPressInterval: const Duration(milliseconds: 150),
                onQuantityChanged: (qty) =>
                    setState(() => _slowLongPressQty = qty),
                onRemove: () => setState(() => _slowLongPressQty = 0),
              ),
            ),
            const SizedBox(height: 8),
            Text(
              'Long-press and hold +/- buttons to see the difference',
              style: TextStyle(
                fontSize: 12,
                color: Colors.grey[600],
                fontStyle: FontStyle.italic,
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'Quantity Formatters',
          'Built-in abbreviation for large numbers',
          [
            _buildRow(
              '1,500 → "1.5k"',
              CartStepper(
                quantity: _abbrev1Qty,
                maxQuantity: 9999999,
                step: 100,
                quantityFormatter: QuantityFormatters.abbreviated,
                onQuantityChanged: (qty) => setState(() => _abbrev1Qty = qty),
                onRemove: () => setState(() => _abbrev1Qty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              '12,500 → "12.5k"',
              CartStepper(
                quantity: _abbrev2Qty,
                maxQuantity: 9999999,
                step: 500,
                quantityFormatter: QuantityFormatters.abbreviated,
                onQuantityChanged: (qty) => setState(() => _abbrev2Qty = qty),
                onRemove: () => setState(() => _abbrev2Qty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              '1,000,000 → "1M"',
              CartStepper(
                quantity: _abbrev3Qty,
                maxQuantity: 9999999,
                step: 100000,
                quantityFormatter: QuantityFormatters.abbreviated,
                onQuantityChanged: (qty) => setState(() => _abbrev3Qty = qty),
                onRemove: () => setState(() => _abbrev3Qty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Max indicator (99+)',
              CartStepper(
                quantity: _maxIndicatorQty,
                maxQuantity: 150,
                quantityFormatter: QuantityFormatters.abbreviatedWithMax(99),
                onQuantityChanged: (qty) =>
                    setState(() => _maxIndicatorQty = qty),
                onRemove: () => setState(() => _maxIndicatorQty = 0),
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'Event Callbacks',
          'onMaxReached, onMinReached, onValidationRejected',
          [
            if (_callbackMessage != null)
              Container(
                padding: const EdgeInsets.all(8),
                margin: const EdgeInsets.only(bottom: 12),
                decoration: BoxDecoration(
                  color: Colors.blue.shade50,
                  borderRadius: BorderRadius.circular(8),
                  border: Border.all(color: Colors.blue.shade200),
                ),
                child: Row(
                  children: [
                    Icon(Icons.info_outline,
                        color: Colors.blue.shade700, size: 20),
                    const SizedBox(width: 8),
                    Expanded(
                      child: Text(
                        _callbackMessage!,
                        style: TextStyle(
                            color: Colors.blue.shade700, fontSize: 12),
                      ),
                    ),
                    IconButton(
                      icon: const Icon(Icons.close, size: 16),
                      onPressed: () => setState(() => _callbackMessage = null),
                      padding: EdgeInsets.zero,
                      constraints: const BoxConstraints(),
                    ),
                  ],
                ),
              ),
            _buildRow(
              'With callbacks (min:1, max:10)',
              CartStepper(
                quantity: _callbacksQty,
                minQuantity: 1,
                maxQuantity: 10,
                onQuantityChanged: (qty) => setState(() => _callbacksQty = qty),
                onMaxReached: () {
                  setState(() => _callbackMessage = 'Max quantity reached!');
                },
                onMinReached: () {
                  setState(() => _callbackMessage = 'Min quantity reached!');
                },
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'Custom Validator',
          'Prevent changes that fail validation',
          [
            _buildRow(
              'Only even numbers',
              CartStepper(
                quantity: _validationQty,
                step: 1,
                maxQuantity: 20,
                validator: (current, next) => next % 2 == 0,
                onQuantityChanged: (qty) =>
                    setState(() => _validationQty = qty),
                onValidationRejected: (current, attempted) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: Text('$attempted is not an even number!'),
                      duration: const Duration(seconds: 1),
                    ),
                  );
                },
                onRemove: () => setState(() => _validationQty = 0),
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'Auto-Collapse',
          'Collapses to a badge after inactivity',
          [
            _buildRow(
              'Auto-Collapse (3s)',
              CartStepper(
                quantity: _autoCollapseQty,
                autoCollapseDelay: const Duration(seconds: 3),
                separateIcon: Icons.shopping_cart,
                onQuantityChanged: (qty) =>
                    setState(() => _autoCollapseQty = qty),
                onRemove: () => setState(() => _autoCollapseQty = 0),
              ),
            ),
            const SizedBox(height: 8),
            Text(
              'Wait 3 seconds to see it collapse to badge view',
              style: TextStyle(
                fontSize: 12,
                color: Colors.grey[600],
                fontStyle: FontStyle.italic,
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'Initially Expanded',
          'Control initial state explicitly',
          [
            _buildRow(
              'initiallyExpanded: true',
              CartStepper(
                quantity: _expandedTrueQty,
                initiallyExpanded: true,
                onQuantityChanged: (qty) =>
                    setState(() => _expandedTrueQty = qty),
                onRemove: () => setState(() => _expandedTrueQty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'initiallyExpanded: false',
              CartStepper(
                quantity: _expandedFalseQty,
                initiallyExpanded: false,
                onQuantityChanged: (qty) =>
                    setState(() => _expandedFalseQty = qty),
                onRemove: () => setState(() => _expandedFalseQty = 0),
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'Custom Icons',
          'Replace default icons',
          [
            _buildRow(
              'Custom increment/decrement',
              CartStepper(
                quantity: _customIconsQty,
                addIcon: Icons.add_circle,
                incrementIcon: Icons.arrow_upward,
                decrementIcon: Icons.arrow_downward,
                deleteIcon: Icons.cancel,
                onQuantityChanged: (qty) =>
                    setState(() => _customIconsQty = qty),
                onRemove: () => setState(() => _customIconsQty = 0),
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'Manual Input',
          'Tap on the quantity to type a value directly',
          [
            _buildRow(
              'Tap to edit (max: 99)',
              CartStepper(
                quantity: _manualInputQty,
                enableManualInput: true,
                onQuantityChanged: (qty) =>
                    setState(() => _manualInputQty = qty),
                onRemove: () => setState(() => _manualInputQty = 0),
                onManualInputSubmitted: (qty) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: Text('Manually set to $qty'),
                      duration: const Duration(seconds: 1),
                    ),
                  );
                },
              ),
            ),
            const SizedBox(height: 8),
            Text(
              'Tap the number in the stepper to open keyboard input',
              style: TextStyle(
                fontSize: 12,
                color: Colors.grey[600],
                fontStyle: FontStyle.italic,
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Large size with manual input',
              CartStepper(
                quantity: _manualInputLargeQty,
                size: CartStepperSize.large,
                maxQuantity: 999,
                enableManualInput: true,
                onQuantityChanged: (qty) =>
                    setState(() => _manualInputLargeQty = qty),
                onRemove: () => setState(() => _manualInputLargeQty = 0),
              ),
            ),
            const SizedBox(height: 16),
            _buildRow(
              'Custom input builder',
              CartStepper(
                quantity: _customInputBuilderQty,
                maxQuantity: 50,
                enableManualInput: true,
                onQuantityChanged: (qty) =>
                    setState(() => _customInputBuilderQty = qty),
                onRemove: () => setState(() => _customInputBuilderQty = 0),
                manualInputBuilder: (context, currentValue, onSubmit, onCancel) {
                  final controller = TextEditingController(text: currentValue.toString());
                  return Container(
                    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                    decoration: BoxDecoration(
                      color: Colors.white.withValues(alpha: 0.2),
                      borderRadius: BorderRadius.circular(8),
                      border: Border.all(color: Colors.white, width: 2),
                    ),
                    child: Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        SizedBox(
                          width: 40,
                          child: TextField(
                            controller: controller,
                            keyboardType: TextInputType.number,
                            textAlign: TextAlign.center,
                            style: const TextStyle(
                              color: Colors.white,
                              fontSize: 14,
                              fontWeight: FontWeight.bold,
                            ),
                            decoration: const InputDecoration(
                              isDense: true,
                              contentPadding: EdgeInsets.zero,
                              border: InputBorder.none,
                            ),
                            autofocus: true,
                            onSubmitted: onSubmit,
                          ),
                        ),
                        GestureDetector(
                          onTap: () => onSubmit(controller.text),
                          child: const Icon(Icons.check, color: Colors.white, size: 16),
                        ),
                      ],
                    ),
                  );
                },
              ),
            ),
            const SizedBox(height: 8),
            Text(
              'Custom builder with a confirm button',
              style: TextStyle(
                fontSize: 12,
                color: Colors.grey[600],
                fontStyle: FontStyle.italic,
              ),
            ),
          ],
        ),
        const SizedBox(height: 60),
      ],
    );
  }
}

// =============================================================================
// TAB 5: COMPONENTS
// =============================================================================
class _ComponentsTab extends StatefulWidget {
  const _ComponentsTab();

  @override
  State<_ComponentsTab> createState() => _ComponentsTabState();
}

class _ComponentsTabState extends State<_ComponentsTab>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  // Controller
  late final CartStepperController _controller;

  // Badge
  int _badgeCount = 5;

  // Group
  List<CartStepperGroupItem> _sizeVariants = [
    const CartStepperGroupItem(quantity: 0, label: 'S'),
    const CartStepperGroupItem(quantity: 1, label: 'M'),
    const CartStepperGroupItem(quantity: 0, label: 'L'),
    const CartStepperGroupItem(quantity: 2, label: 'XL'),
  ];

  // Themed
  int _themed1Qty = 2;
  int _themed2Qty = 1;

  @override
  void initState() {
    super.initState();
    _controller = CartStepperController(
      initialQuantity: 3,
      minQuantity: 0,
      maxQuantity: 10,
      onMaxReached: () => _showMessage('Controller: Max reached!'),
      onMinReached: () => _showMessage('Controller: Min reached!'),
    );
    _controller.addListener(_onControllerChanged);
  }

  void _onControllerChanged() {
    if (mounted) setState(() {});
  }

  void _showMessage(String message) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message), duration: const Duration(seconds: 1)),
    );
  }

  @override
  void dispose() {
    _controller.removeListener(_onControllerChanged);
    _controller.dispose();
    super.dispose();
  }

  int get _totalVariants =>
      _sizeVariants.fold(0, (sum, item) => sum + item.quantity);

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        _buildSection(
          context,
          'CartStepperController',
          'External state management with ChangeNotifier',
          [
            _buildRow(
              'Controlled (qty: ${_controller.quantity})',
              CartStepper(
                quantity: _controller.quantity,
                maxQuantity: _controller.maxQuantity,
                onQuantityChanged: _controller.setQuantity,
                onRemove: _controller.reset,
              ),
            ),
            const SizedBox(height: 16),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              alignment: WrapAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: _controller.reset,
                  child: const Text('Reset'),
                ),
                ElevatedButton(
                  onPressed: _controller.canIncrement
                      ? () => _controller.setQuantity(5)
                      : null,
                  child: const Text('Set to 5'),
                ),
                ElevatedButton(
                  onPressed: _controller.canIncrement
                      ? () => _controller.setQuantity(10)
                      : null,
                  child: const Text('Set to Max'),
                ),
              ],
            ),
            const SizedBox(height: 12),
            Container(
              padding: const EdgeInsets.all(8),
              decoration: BoxDecoration(
                color: Colors.grey.shade100,
                borderRadius: BorderRadius.circular(8),
              ),
              child: Text(
                'canIncrement: ${_controller.canIncrement}\n'
                'canDecrement: ${_controller.canDecrement}\n'
                'isAtMin: ${_controller.isAtMin}\n'
                'isAtMax: ${_controller.isAtMax}',
                style: const TextStyle(fontSize: 12, fontFamily: 'monospace'),
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'CartBadge',
          'Display cart count on icons',
          [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                CartBadge(
                  count: _badgeCount,
                  child: const Icon(Icons.shopping_cart, size: 32),
                ),
                CartBadge(
                  count: _badgeCount,
                  badgeColor: Colors.blue,
                  alignment: Alignment.topLeft,
                  child: const Icon(Icons.shopping_bag, size: 32),
                ),
                CartBadge(
                  count: 150,
                  maxCount: 99,
                  child: const Icon(Icons.favorite, size: 32),
                ),
              ],
            ),
            const SizedBox(height: 16),
            Slider(
              value: _badgeCount.toDouble(),
              min: 0,
              max: 150,
              divisions: 150,
              label: '$_badgeCount',
              onChanged: (v) => setState(() => _badgeCount = v.toInt()),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'CartStepperGroup',
          'Horizontal row for variant selection',
          [
            CartStepperGroup(
              items: _sizeVariants,
              size: CartStepperSize.compact,
              maxTotalQuantity: 10,
              onQuantityChanged: (index, qty) {
                setState(() {
                  _sizeVariants = List.from(_sizeVariants);
                  _sizeVariants[index] =
                      _sizeVariants[index].copyWith(quantity: qty);
                });
              },
              onRemove: (index) {
                setState(() {
                  _sizeVariants = List.from(_sizeVariants);
                  _sizeVariants[index] =
                      _sizeVariants[index].copyWith(quantity: 0);
                });
              },
            ),
            const SizedBox(height: 12),
            Text(
              'Total: $_totalVariants / 10',
              style: Theme.of(context).textTheme.bodyMedium,
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'CartStepperTheme',
          'Theme multiple steppers consistently',
          [
            CartStepperTheme(
              data: const CartStepperThemeData(
                style: CartStepperStyle(
                  backgroundColor: Color(0xFF6B4EE6),
                  foregroundColor: Colors.white,
                  borderColor: Color(0xFF6B4EE6),
                ),
                size: CartStepperSize.normal,
              ),
              child: Column(
                children: [
                  _buildRow(
                    'Themed 1',
                    ThemedCartStepper(
                      quantity: _themed1Qty,
                      onQuantityChanged: (qty) =>
                          setState(() => _themed1Qty = qty),
                      onRemove: () => setState(() => _themed1Qty = 0),
                    ),
                  ),
                  const SizedBox(height: 16),
                  _buildRow(
                    'Themed 2',
                    ThemedCartStepper(
                      quantity: _themed2Qty,
                      onQuantityChanged: (qty) =>
                          setState(() => _themed2Qty = qty),
                      onRemove: () => setState(() => _themed2Qty = 0),
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
        const SizedBox(height: 24),
        _buildSection(
          context,
          'Standalone Components',
          'Use individual widgets separately',
          [
            const Text(
              'AnimatedCounter:',
              style: TextStyle(fontWeight: FontWeight.w600),
            ),
            const SizedBox(height: 8),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                AnimatedCounter(
                  value: _badgeCount,
                  style: const TextStyle(
                    fontSize: 32,
                    fontWeight: FontWeight.bold,
                    color: Color(0xFFD84315),
                  ),
                ),
                AnimatedCounter(
                  value: _badgeCount,
                  style: const TextStyle(fontSize: 24),
                  formatter: QuantityFormatters.abbreviated,
                ),
              ],
            ),
            const SizedBox(height: 16),
            const Text(
              'StepperButton:',
              style: TextStyle(fontWeight: FontWeight.w600),
            ),
            const SizedBox(height: 8),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                Container(
                  decoration: BoxDecoration(
                    color: const Color(0xFFD84315),
                    borderRadius: BorderRadius.circular(22),
                  ),
                  child: StepperButton(
                    icon: Icons.remove,
                    iconSize: 24,
                    iconColor: Colors.white,
                    enabled: _badgeCount > 0,
                    onTap: () =>
                        setState(() => _badgeCount = (_badgeCount - 1).clamp(0, 150)),
                    onLongPressStart: () {},
                    onLongPressEnd: () {},
                    size: 44,
                    splashColor: Colors.white24,
                    highlightColor: Colors.white12,
                  ),
                ),
                Container(
                  width: 44,
                  height: 44,
                  decoration: BoxDecoration(
                    color: const Color(0xFFD84315),
                    borderRadius: BorderRadius.circular(22),
                  ),
                  child: Center(
                    child: Text(
                      '$_badgeCount',
                      style: const TextStyle(
                        color: Colors.white,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                ),
                Container(
                  decoration: BoxDecoration(
                    color: const Color(0xFFD84315),
                    borderRadius: BorderRadius.circular(22),
                  ),
                  child: StepperButton(
                    icon: Icons.add,
                    iconSize: 24,
                    iconColor: Colors.white,
                    enabled: _badgeCount < 150,
                    onTap: () =>
                        setState(() => _badgeCount = (_badgeCount + 1).clamp(0, 150)),
                    onLongPressStart: () {},
                    onLongPressEnd: () {},
                    size: 44,
                    splashColor: Colors.white24,
                    highlightColor: Colors.white12,
                  ),
                ),
              ],
            ),
          ],
        ),
        const SizedBox(height: 60),
      ],
    );
  }
}

// =============================================================================
// TAB 6: REAL WORLD
// =============================================================================
class _RealWorldTab extends StatefulWidget {
  const _RealWorldTab();

  @override
  State<_RealWorldTab> createState() => _RealWorldTabState();
}

class _RealWorldTabState extends State<_RealWorldTab>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  final List<_Product> _products = [
    _Product(
      id: '1',
      name: 'iPhone 15 Pro',
      description: '256GB, Natural Titanium',
      price: 999.00,
      quantity: 1,
      image: Icons.phone_iphone,
    ),
    _Product(
      id: '2',
      name: 'AirPods Pro',
      description: '2nd Generation',
      price: 249.00,
      quantity: 2,
      image: Icons.headphones,
    ),
    _Product(
      id: '3',
      name: 'MacBook Air M3',
      description: '13", 8GB RAM, 256GB SSD',
      price: 1299.00,
      quantity: 0,
      image: Icons.laptop_mac,
    ),
    _Product(
      id: '4',
      name: 'Apple Watch Ultra 2',
      description: '49mm, Titanium Case',
      price: 799.00,
      quantity: 0,
      image: Icons.watch,
    ),
    _Product(
      id: '5',
      name: 'iPad Pro 12.9"',
      description: 'M4 chip, 256GB, Wi-Fi',
      price: 1099.00,
      quantity: 1,
      image: Icons.tablet_mac,
    ),
  ];

  int get _totalItems =>
      _products.fold(0, (sum, product) => sum + product.quantity);

  double get _totalPrice => _products.fold(
      0.0, (sum, product) => sum + (product.price * product.quantity));

  void _updateQuantity(int index, int qty) {
    setState(() {
      _products[index] = _products[index].copyWith(quantity: qty);
    });
  }

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

    return Column(
      children: [
        // Cart summary header
        Container(
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            color: theme.colorScheme.primaryContainer.withValues(alpha: 0.3),
            border: Border(
              bottom: BorderSide(color: theme.dividerColor),
            ),
          ),
          child: Row(
            children: [
              CartBadge(
                count: _totalItems,
                child: Icon(
                  Icons.shopping_cart,
                  size: 32,
                  color: theme.colorScheme.primary,
                ),
              ),
              const SizedBox(width: 16),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Shopping Cart',
                      style: theme.textTheme.titleMedium?.copyWith(
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    Text(
                      '$_totalItems items',
                      style: theme.textTheme.bodySmall?.copyWith(
                        color: Colors.grey[600],
                      ),
                    ),
                  ],
                ),
              ),
              Column(
                crossAxisAlignment: CrossAxisAlignment.end,
                children: [
                  Text(
                    'Total',
                    style: theme.textTheme.bodySmall?.copyWith(
                      color: Colors.grey[600],
                    ),
                  ),
                  Text(
                    '\$${_totalPrice.toStringAsFixed(2)}',
                    style: theme.textTheme.titleLarge?.copyWith(
                      fontWeight: FontWeight.bold,
                      color: theme.colorScheme.primary,
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),

        // Product list
        Expanded(
          child: ListView.separated(
            padding: const EdgeInsets.all(16),
            itemCount: _products.length,
            separatorBuilder: (_, __) => const SizedBox(height: 12),
            itemBuilder: (context, index) {
              final product = _products[index];
              return CartProductTile(
                leading: Container(
                  width: 64,
                  height: 64,
                  decoration: BoxDecoration(
                    color: Colors.grey[200],
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Icon(
                    product.image,
                    color: Colors.grey[600],
                    size: 32,
                  ),
                ),
                title: product.name,
                subtitle: product.description,
                price: '\$${product.price.toStringAsFixed(2)}',
                quantity: product.quantity,
                minQuantity: 0,
                maxQuantity: 10,
                stepperSize: CartStepperSize.compact,
                onQuantityChanged: (qty) => _updateQuantity(index, qty),
                onRemove: () => _updateQuantity(index, 0),
              );
            },
          ),
        ),

        // Checkout button
        Container(
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            color: theme.scaffoldBackgroundColor,
            boxShadow: [
              BoxShadow(
                color: Colors.black.withValues(alpha: 0.1),
                blurRadius: 8,
                offset: const Offset(0, -2),
              ),
            ],
          ),
          child: SafeArea(
            child: SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: _totalItems > 0
                    ? () {
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(
                            content: Text(
                              'Checkout: $_totalItems items for \$${_totalPrice.toStringAsFixed(2)}',
                            ),
                          ),
                        );
                      }
                    : null,
                style: ElevatedButton.styleFrom(
                  backgroundColor: theme.colorScheme.primary,
                  foregroundColor: theme.colorScheme.onPrimary,
                  padding: const EdgeInsets.symmetric(vertical: 16),
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
                child: Text(
                  _totalItems > 0
                      ? 'Checkout (\$${_totalPrice.toStringAsFixed(2)})'
                      : 'Cart is Empty',
                  style: const TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

// =============================================================================
// HELPERS
// =============================================================================
Widget _buildSection(
  BuildContext context,
  String title,
  String description,
  List<Widget> children,
) {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(
        title,
        style: Theme.of(context).textTheme.titleLarge?.copyWith(
              fontWeight: FontWeight.bold,
            ),
      ),
      const SizedBox(height: 4),
      Text(
        description,
        style: Theme.of(context).textTheme.bodyMedium?.copyWith(
              color: Colors.grey[600],
            ),
      ),
      const SizedBox(height: 16),
      Container(
        padding: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: Colors.grey[50],
          borderRadius: BorderRadius.circular(12),
          border: Border.all(color: Colors.grey[200]!),
        ),
        child: Column(children: children),
      ),
    ],
  );
}

Widget _buildRow(String label, Widget stepper) {
  return Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
      Flexible(
        child: Text(label, style: const TextStyle(fontSize: 14)),
      ),
      stepper,
    ],
  );
}

// =============================================================================
// MODELS
// =============================================================================
class _Product {
  final String id;
  final String name;
  final String description;
  final double price;
  final int quantity;
  final IconData image;

  const _Product({
    required this.id,
    required this.name,
    required this.description,
    required this.price,
    required this.quantity,
    required this.image,
  });

  _Product copyWith({
    String? id,
    String? name,
    String? description,
    double? price,
    int? quantity,
    IconData? image,
  }) {
    return _Product(
      id: id ?? this.id,
      name: name ?? this.name,
      description: description ?? this.description,
      price: price ?? this.price,
      quantity: quantity ?? this.quantity,
      image: image ?? this.image,
    );
  }
}
1
likes
160
points
533
downloads

Publisher

unverified uploader

Weekly Downloads

A customizable expandable cart quantity stepper widget for Flutter with async support, loading indicators, and theming.

Repository (GitHub)
View/report issues

Topics

#cart #stepper #quantity #e-commerce #widget

Documentation

API reference

License

MIT (license)

Dependencies

flutter, flutter_spinkit

More

Packages that depend on advance_cart_stepper