reaction_overlay 0.1.0 copy "reaction_overlay: ^0.1.0" to clipboard
reaction_overlay: ^0.1.0 copied to clipboard

A floating reaction bar (like iMessage/WhatsApp) for Flutter. Wrap any widget to add emoji reactions with animations, haptic feedback, dark mode, and smart positioning.

example/lib/main.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:reaction_overlay/reaction_overlay.dart';

void main() => runApp(const MyApp());

// ─── Color Palette ───────────────────────────────────────────────────────────

class _AppColors {
  static const accent = Color(0xFF007AFF);
  static const cardLight = Color(0xFFFFFFFF);
  static const cardDark = Color(0xFF1C1C1E);
  static const bgLight = Color(0xFFF2F2F7);
  static const bgDark = Color(0xFF000000);
  static const subtitleLight = Color(0xFF8E8E93);
  static const subtitleDark = Color(0xFF98989D);
  static const separatorLight = Color(0xFFC6C6C8);
  static const separatorDark = Color(0xFF38383A);
}

// ─── App ─────────────────────────────────────────────────────────────────────

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Reaction Overlay',
      debugShowCheckedModeBanner: false,
      themeMode: ThemeMode.system,
      theme: ThemeData(
        brightness: Brightness.light,
        scaffoldBackgroundColor: _AppColors.bgLight,
        colorScheme: ColorScheme.fromSeed(
          seedColor: _AppColors.accent,
          brightness: Brightness.light,
        ),
        appBarTheme: const AppBarTheme(
          backgroundColor: Colors.transparent,
          elevation: 0,
          scrolledUnderElevation: 0,
        ),
        extensions: const [
          ReactionOverlayThemeData(
            curve: Curves.easeOutBack,
            dimBackground: true,
            enableHapticFeedback: true,
            triggerMode: TriggerMode.longPress,
            borderRadius: BorderRadius.all(Radius.circular(28)),
            padding: EdgeInsets.symmetric(horizontal: 6, vertical: 6),
          ),
        ],
      ),
      darkTheme: ThemeData(
        brightness: Brightness.dark,
        scaffoldBackgroundColor: _AppColors.bgDark,
        colorScheme: ColorScheme.fromSeed(
          seedColor: _AppColors.accent,
          brightness: Brightness.dark,
        ),
        appBarTheme: const AppBarTheme(
          backgroundColor: Colors.transparent,
          elevation: 0,
          scrolledUnderElevation: 0,
        ),
        extensions: const [
          ReactionOverlayThemeData(
            curve: Curves.easeOutBack,
            dimBackground: true,
            enableHapticFeedback: true,
            triggerMode: TriggerMode.longPress,
            borderRadius: BorderRadius.all(Radius.circular(28)),
            padding: EdgeInsets.symmetric(horizontal: 6, vertical: 6),
          ),
        ],
      ),
      home: const _HomePage(),
    );
  }
}

// ─── Reactions ───────────────────────────────────────────────────────────────

List<Reaction> _buildReactions(BuildContext context, {VoidCallback? onAny}) => [
      Reaction('👍', label: 'Like', onTap: onAny),
      Reaction('❤️', label: 'Love', onTap: onAny),
      Reaction('😂', label: 'Haha', onTap: onAny),
      Reaction('😮', label: 'Wow', onTap: onAny),
      Reaction('😢', label: 'Sad', onTap: onAny),
      Reaction('🔥', label: 'Fire', onTap: onAny),
    ];

// ─── Home Page ───────────────────────────────────────────────────────────────

class _HomePage extends StatefulWidget {
  const _HomePage();

  @override
  State<_HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<_HomePage> {
  final _controller = ReactionOverlayController();
  int _currentTab = 0;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentTab,
        children: [
          _MessagesTab(controller: _controller),
          const _FeaturesTab(),
          const _SettingsTab(),
        ],
      ),
      bottomNavigationBar: _buildBottomNav(context),
    );
  }

  Widget _buildBottomNav(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    return Container(
      decoration: BoxDecoration(
        border: Border(
          top: BorderSide(
            color: isDark ? _AppColors.separatorDark : _AppColors.separatorLight,
            width: 0.5,
          ),
        ),
      ),
      child: NavigationBar(
        selectedIndex: _currentTab,
        onDestinationSelected: (i) => setState(() => _currentTab = i),
        backgroundColor: isDark ? const Color(0xFF1C1C1E) : Colors.white,
        indicatorColor: _AppColors.accent.withValues(alpha: 0.12),
        destinations: const [
          NavigationDestination(
            icon: Icon(CupertinoIcons.chat_bubble_2),
            selectedIcon: Icon(CupertinoIcons.chat_bubble_2_fill),
            label: 'Messages',
          ),
          NavigationDestination(
            icon: Icon(CupertinoIcons.sparkles),
            selectedIcon: Icon(CupertinoIcons.sparkles),
            label: 'Features',
          ),
          NavigationDestination(
            icon: Icon(CupertinoIcons.gear),
            selectedIcon: Icon(CupertinoIcons.gear_solid),
            label: 'Settings',
          ),
        ],
      ),
    );
  }
}

// ─── Tab 1: Messages (Chat UI) ──────────────────────────────────────────────

class _MessagesTab extends StatefulWidget {
  const _MessagesTab({required this.controller});
  final ReactionOverlayController controller;

  @override
  State<_MessagesTab> createState() => _MessagesTabState();
}

class _MessagesTabState extends State<_MessagesTab> {
  final List<_ChatMessage> _messages = _ChatMessage.samples;
  final Map<int, String> _messageReactions = {};

  void _onReaction(int messageIndex, String emoji) {
    setState(() {
      if (_messageReactions[messageIndex] == emoji) {
        _messageReactions.remove(messageIndex);
      } else {
        _messageReactions[messageIndex] = emoji;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        const CupertinoSliverNavigationBar(
          largeTitle: Text('Messages'),
          border: null,
        ),
        SliverPadding(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          sliver: SliverList.builder(
            itemCount: _messages.length,
            itemBuilder: (context, index) {
              final msg = _messages[index];
              final reaction = _messageReactions[index];
              return _ChatBubble(
                message: msg,
                reaction: reaction,
                onReaction: (emoji) => _onReaction(index, emoji),
              );
            },
          ),
        ),
        const SliverToBoxAdapter(child: SizedBox(height: 100)),
      ],
    );
  }
}

class _ChatMessage {
  const _ChatMessage({
    required this.text,
    required this.isMe,
    required this.time,
    this.avatar,
  });

  final String text;
  final bool isMe;
  final String time;
  final String? avatar;

  static final samples = [
    const _ChatMessage(
      text: 'Hey! Have you seen the new reaction overlay package? 🎉',
      isMe: false,
      time: '9:41 AM',
      avatar: 'A',
    ),
    const _ChatMessage(
      text: 'Yes! It looks amazing. Long press on any message to try it out.',
      isMe: true,
      time: '9:42 AM',
    ),
    const _ChatMessage(
      text: 'The animations are so smooth 🤩',
      isMe: false,
      time: '9:42 AM',
      avatar: 'A',
    ),
    const _ChatMessage(
      text: 'I love how you can customize everything — triggers, positions, themes...',
      isMe: true,
      time: '9:43 AM',
    ),
    const _ChatMessage(
      text: 'Can you show me the badge feature?',
      isMe: false,
      time: '9:44 AM',
      avatar: 'A',
    ),
    const _ChatMessage(
      text: 'Sure! React to any message and you\'ll see the badge appear below it.',
      isMe: true,
      time: '9:44 AM',
    ),
    const _ChatMessage(
      text: 'What about different trigger modes?',
      isMe: false,
      time: '9:45 AM',
      avatar: 'A',
    ),
    const _ChatMessage(
      text: 'Check the Features tab — there are demos for tap, double tap, and more!',
      isMe: true,
      time: '9:45 AM',
    ),
  ];
}

class _ChatBubble extends StatelessWidget {
  const _ChatBubble({
    required this.message,
    required this.onReaction,
    this.reaction,
  });

  final _ChatMessage message;
  final String? reaction;
  final void Function(String emoji) onReaction;

  @override
  Widget build(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;

    final bubbleColor = message.isMe
        ? _AppColors.accent
        : (isDark ? _AppColors.cardDark : const Color(0xFFE9E9EB));
    final textColor = message.isMe
        ? Colors.white
        : (isDark ? Colors.white : Colors.black);
    final timeColor = message.isMe
        ? Colors.white70
        : (isDark ? _AppColors.subtitleDark : _AppColors.subtitleLight);

    return Padding(
      padding: const EdgeInsets.only(bottom: 4),
      child: Row(
        mainAxisAlignment:
            message.isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          if (!message.isMe) ...[
            _Avatar(letter: message.avatar ?? '?'),
            const SizedBox(width: 8),
          ],
          Flexible(
            child: Column(
              crossAxisAlignment: message.isMe
                  ? CrossAxisAlignment.end
                  : CrossAxisAlignment.start,
              children: [
                ReactionTarget(
                  reactions: _buildReactions(context, onAny: null).map((r) {
                    return Reaction(
                      r.emoji!,
                      label: r.label,
                      onTap: () => onReaction(r.emoji!),
                    );
                  }).toList(),
                  onEmojiSelected: onReaction,
                  child: Container(
                    constraints: BoxConstraints(
                      maxWidth: MediaQuery.of(context).size.width * 0.72,
                    ),
                    padding: const EdgeInsets.symmetric(
                        horizontal: 16, vertical: 10),
                    decoration: BoxDecoration(
                      color: bubbleColor,
                      borderRadius: BorderRadius.only(
                        topLeft: const Radius.circular(20),
                        topRight: const Radius.circular(20),
                        bottomLeft:
                            Radius.circular(message.isMe ? 20 : 6),
                        bottomRight:
                            Radius.circular(message.isMe ? 6 : 20),
                      ),
                    ),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.end,
                      children: [
                        Text(
                          message.text,
                          style: TextStyle(
                            color: textColor,
                            fontSize: 16,
                            height: 1.35,
                          ),
                        ),
                        const SizedBox(height: 2),
                        Text(
                          message.time,
                          style: TextStyle(color: timeColor, fontSize: 11),
                        ),
                      ],
                    ),
                  ),
                ),
                if (reaction != null)
                  Padding(
                    padding: const EdgeInsets.only(top: 2),
                    child: Container(
                      padding: const EdgeInsets.symmetric(
                          horizontal: 6, vertical: 2),
                      decoration: BoxDecoration(
                        color: isDark
                            ? _AppColors.cardDark
                            : _AppColors.cardLight,
                        borderRadius: BorderRadius.circular(12),
                        boxShadow: [
                          BoxShadow(
                            color: Colors.black.withValues(alpha: 0.08),
                            blurRadius: 4,
                            offset: const Offset(0, 1),
                          ),
                        ],
                      ),
                      child: Text(reaction!, style: const TextStyle(fontSize: 18)),
                    ),
                  ),
              ],
            ),
          ),
          if (message.isMe) const SizedBox(width: 8),
        ],
      ),
    );
  }
}

class _Avatar extends StatelessWidget {
  const _Avatar({required this.letter});
  final String letter;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 32,
      height: 32,
      decoration: BoxDecoration(
        gradient: const LinearGradient(
          colors: [Color(0xFFFF6B6B), Color(0xFFFF8E53)],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
        borderRadius: BorderRadius.circular(16),
      ),
      alignment: Alignment.center,
      child: Text(
        letter,
        style: const TextStyle(
          color: Colors.white,
          fontWeight: FontWeight.w600,
          fontSize: 14,
        ),
      ),
    );
  }
}

// ─── Tab 2: Features Showcase ────────────────────────────────────────────────

class _FeaturesTab extends StatefulWidget {
  const _FeaturesTab();

  @override
  State<_FeaturesTab> createState() => _FeaturesTabState();
}

class _FeaturesTabState extends State<_FeaturesTab> {
  final _controller = ReactionOverlayController();
  final Map<int, String> _badgeSelections = {};

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

  @override
  Widget build(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    final cardColor = isDark ? _AppColors.cardDark : _AppColors.cardLight;

    return CustomScrollView(
      slivers: [
        const CupertinoSliverNavigationBar(
          largeTitle: Text('Features'),
          border: null,
        ),
        SliverPadding(
          padding: const EdgeInsets.all(16),
          sliver: SliverList.list(
            children: [
              // ── Trigger modes ──
              _SectionTitle(title: 'TRIGGER MODES', icon: CupertinoIcons.hand_raised),
              _Card(
                color: cardColor,
                children: [
                  _FeatureRow(
                    icon: CupertinoIcons.hand_point_right,
                    iconColor: const Color(0xFF007AFF),
                    title: 'Long Press',
                    subtitle: 'Default behavior',
                    child: ReactionTarget(
                      reactions: _buildReactions(context),
                      child: const _TryButton(label: 'Hold'),
                    ),
                  ),
                  _CardDivider(isDark: isDark),
                  _FeatureRow(
                    icon: CupertinoIcons.cursor_rays,
                    iconColor: const Color(0xFF34C759),
                    title: 'Single Tap',
                    subtitle: 'Quick access',
                    child: ReactionTarget(
                      reactions: _buildReactions(context),
                      triggerMode: TriggerMode.tap,
                      child: const _TryButton(label: 'Tap'),
                    ),
                  ),
                  _CardDivider(isDark: isDark),
                  _FeatureRow(
                    icon: CupertinoIcons.hand_draw,
                    iconColor: const Color(0xFFFF9500),
                    title: 'Double Tap',
                    subtitle: 'Instagram style',
                    child: ReactionTarget(
                      reactions: _buildReactions(context),
                      triggerMode: TriggerMode.doubleTap,
                      child: const _TryButton(label: '2×Tap'),
                    ),
                  ),
                ],
              ),

              const SizedBox(height: 24),

              // ── Positions ──
              _SectionTitle(title: 'OVERLAY POSITIONS', icon: CupertinoIcons.arrow_up_arrow_down),
              _Card(
                color: cardColor,
                children: [
                  _FeatureRow(
                    icon: CupertinoIcons.arrow_up,
                    iconColor: const Color(0xFF5856D6),
                    title: 'Above',
                    subtitle: 'Show on top',
                    child: ReactionTarget(
                      reactions: _buildReactions(context),
                      position: OverlayPosition.above,
                      child: const _TryButton(label: 'Try'),
                    ),
                  ),
                  _CardDivider(isDark: isDark),
                  _FeatureRow(
                    icon: CupertinoIcons.arrow_down,
                    iconColor: const Color(0xFFAF52DE),
                    title: 'Below',
                    subtitle: 'Show underneath',
                    child: ReactionTarget(
                      reactions: _buildReactions(context),
                      position: OverlayPosition.below,
                      child: const _TryButton(label: 'Try'),
                    ),
                  ),
                  _CardDivider(isDark: isDark),
                  _FeatureRow(
                    icon: CupertinoIcons.arrow_right,
                    iconColor: const Color(0xFFFF2D55),
                    title: 'Right',
                    subtitle: 'Horizontal layout',
                    child: ReactionTarget(
                      reactions: _buildReactions(context),
                      position: OverlayPosition.right,
                      child: const _TryButton(label: 'Try'),
                    ),
                  ),
                  _CardDivider(isDark: isDark),
                  _FeatureRow(
                    icon: CupertinoIcons.arrow_left,
                    iconColor: const Color(0xFFFF3B30),
                    title: 'Left',
                    subtitle: 'Horizontal layout',
                    child: ReactionTarget(
                      reactions: _buildReactions(context),
                      position: OverlayPosition.left,
                      child: const _TryButton(label: 'Try'),
                    ),
                  ),
                ],
              ),

              const SizedBox(height: 24),

              // ── Controller ──
              _SectionTitle(title: 'CONTROLLER', icon: CupertinoIcons.slider_horizontal_3),
              _Card(
                color: cardColor,
                children: [
                  Builder(
                    builder: (ctx) => _FeatureRow(
                      icon: CupertinoIcons.play_circle,
                      iconColor: const Color(0xFF007AFF),
                      title: 'Programmatic',
                      subtitle: 'Open via controller',
                      child: GestureDetector(
                        onTap: () {
                          _controller.show(
                            context: ctx,
                            reactions: _buildReactions(context),
                          );
                        },
                        child: const _TryButton(label: 'Open'),
                      ),
                    ),
                  ),
                ],
              ),

              const SizedBox(height: 24),

              // ── Badges ──
              _SectionTitle(title: 'REACTION BADGES', icon: CupertinoIcons.heart_circle),
              _Card(
                color: cardColor,
                children: [
                  Padding(
                    padding: const EdgeInsets.all(16),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          'Tap badges to toggle selection',
                          style: TextStyle(
                            color: isDark
                                ? _AppColors.subtitleDark
                                : _AppColors.subtitleLight,
                            fontSize: 13,
                          ),
                        ),
                        const SizedBox(height: 12),
                        ReactionBadgeBar(
                          badges: [
                            ReactionBadgeData(
                              reaction: Reaction('👍'),
                              count: 24,
                              isSelected: _badgeSelections.containsKey(0),
                            ),
                            ReactionBadgeData(
                              reaction: Reaction('❤️'),
                              count: 18,
                              isSelected: _badgeSelections.containsKey(1),
                            ),
                            ReactionBadgeData(
                              reaction: Reaction('😂'),
                              count: 7,
                              isSelected: _badgeSelections.containsKey(2),
                            ),
                            ReactionBadgeData(
                              reaction: Reaction('🔥'),
                              count: 3,
                              isSelected: _badgeSelections.containsKey(3),
                            ),
                          ],
                          onTap: (index, data) {
                            setState(() {
                              if (_badgeSelections.containsKey(index)) {
                                _badgeSelections.remove(index);
                              } else {
                                _badgeSelections[index] =
                                    data.reaction.emoji ?? '';
                              }
                            });
                          },
                        ),
                      ],
                    ),
                  ),
                ],
              ),

              const SizedBox(height: 24),

              // ── Custom Widgets ──
              _SectionTitle(title: 'CUSTOM REACTIONS', icon: CupertinoIcons.paintbrush),
              _Card(
                color: cardColor,
                children: [
                  ReactionTarget(
                    reactions: [
                      Reaction.custom(
                        child: const Icon(CupertinoIcons.heart_fill,
                            color: Color(0xFFFF2D55), size: 24),
                        label: 'Love',
                      ),
                      Reaction.custom(
                        child: const Icon(CupertinoIcons.star_fill,
                            color: Color(0xFFFFCC00), size: 24),
                        label: 'Star',
                      ),
                      Reaction.custom(
                        child: const Icon(CupertinoIcons.bolt_fill,
                            color: Color(0xFFFF9500), size: 24),
                        label: 'Bolt',
                      ),
                      Reaction.custom(
                        child: const Icon(CupertinoIcons.flame_fill,
                            color: Color(0xFFFF3B30), size: 24),
                        label: 'Fire',
                      ),
                      Reaction.custom(
                        child: const Icon(CupertinoIcons.hand_thumbsup_fill,
                            color: Color(0xFF007AFF), size: 24),
                        label: 'Thumbs Up',
                      ),
                    ],
                    child: Padding(
                      padding: const EdgeInsets.all(16),
                      child: Row(
                        children: [
                          Container(
                            width: 40,
                            height: 40,
                            decoration: BoxDecoration(
                              color: const Color(0xFF5856D6).withValues(alpha: 0.12),
                              borderRadius: BorderRadius.circular(10),
                            ),
                            child: const Icon(CupertinoIcons.paintbrush_fill,
                                color: Color(0xFF5856D6), size: 20),
                          ),
                          const SizedBox(width: 14),
                          Expanded(
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(
                                  'Icon Reactions',
                                  style: TextStyle(
                                    fontSize: 16,
                                    fontWeight: FontWeight.w500,
                                    color: isDark ? Colors.white : Colors.black,
                                  ),
                                ),
                                Text(
                                  'Long press — uses custom widgets',
                                  style: TextStyle(
                                    fontSize: 13,
                                    color: isDark
                                        ? _AppColors.subtitleDark
                                        : _AppColors.subtitleLight,
                                  ),
                                ),
                              ],
                            ),
                          ),
                          Icon(
                            CupertinoIcons.chevron_right,
                            size: 14,
                            color: isDark
                                ? _AppColors.subtitleDark
                                : _AppColors.subtitleLight,
                          ),
                        ],
                      ),
                    ),
                  ),
                ],
              ),

              const SizedBox(height: 24),

              // ── Emoji Panel ──
              _SectionTitle(title: 'EMOJI PANEL', icon: CupertinoIcons.smiley),
              _Card(
                color: cardColor,
                children: [
                  _FeatureRow(
                    icon: CupertinoIcons.smiley_fill,
                    iconColor: const Color(0xFFFF9500),
                    title: 'Full Emoji Picker',
                    subtitle: 'Open standalone panel',
                    child: GestureDetector(
                      onTap: () async {
                        final emoji = await ReactionEmojiPanel.show(context);
                        if (emoji != null && context.mounted) {
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(
                              content: Text('Selected: $emoji'),
                              duration: const Duration(seconds: 1),
                            ),
                          );
                        }
                      },
                      child: const _TryButton(label: 'Open'),
                    ),
                  ),
                  _CardDivider(isDark: isDark),
                  _FeatureRow(
                    icon: CupertinoIcons.add_circled,
                    iconColor: const Color(0xFF34C759),
                    title: 'Integrated (+)',
                    subtitle: 'Long press, then tap +',
                    child: ReactionTarget(
                      reactions: _buildReactions(context),
                      showExpandButton: true,
                      onEmojiSelected: (emoji) {
                        if (context.mounted) {
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(
                              content: Text('Panel emoji: $emoji'),
                              duration: const Duration(seconds: 1),
                            ),
                          );
                        }
                      },
                      child: const _TryButton(label: 'Hold'),
                    ),
                  ),
                  _CardDivider(isDark: isDark),
                  _FeatureRow(
                    icon: CupertinoIcons.minus_circled,
                    iconColor: const Color(0xFFFF3B30),
                    title: 'No Expand',
                    subtitle: 'Hidden + button',
                    child: ReactionTarget(
                      reactions: _buildReactions(context),
                      showExpandButton: false,
                      child: const _TryButton(label: 'Hold'),
                    ),
                  ),
                ],
              ),

              const SizedBox(height: 24),

              // ── New Features ──
              _SectionTitle(title: 'ADVANCED FEATURES', icon: CupertinoIcons.sparkles),
              _Card(
                color: cardColor,
                children: [
                  _FeatureRow(
                    icon: CupertinoIcons.hand_draw,
                    iconColor: const Color(0xFFFF2D55),
                    title: 'Drag Preview',
                    subtitle: 'Drag over emojis to magnify',
                    child: ReactionTarget(
                      reactions: _buildReactions(context),
                      enableDragPreview: true,
                      showPreviewLabel: true,
                      child: const _TryButton(label: 'Hold'),
                    ),
                  ),
                  _CardDivider(isDark: isDark),
                  _FeatureRow(
                    icon: CupertinoIcons.burst,
                    iconColor: const Color(0xFFFF9500),
                    title: 'Particles',
                    subtitle: 'Burst animation on select',
                    child: ReactionTarget(
                      reactions: _buildReactions(context),
                      enableSelectionParticles: true,
                      child: const _TryButton(label: 'Hold'),
                    ),
                  ),
                  _CardDivider(isDark: isDark),
                  _FeatureRow(
                    icon: CupertinoIcons.bubble_left_fill,
                    iconColor: const Color(0xFF5856D6),
                    title: 'Bubble Arrow',
                    subtitle: 'Arrow pointing to target',
                    child: ReactionTarget(
                      reactions: _buildReactions(context),
                      showBubbleArrow: true,
                      child: const _TryButton(label: 'Hold'),
                    ),
                  ),
                  _CardDivider(isDark: isDark),
                  _FeatureRow(
                    icon: CupertinoIcons.checkmark_circle_fill,
                    iconColor: const Color(0xFF34C759),
                    title: 'Multi-Select',
                    subtitle: 'Select multiple reactions',
                    child: ReactionTarget(
                      reactions: _buildReactions(context),
                      allowMultipleSelections: true,
                      onReactionChanged: (index, reaction) {
                        if (context.mounted) {
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(
                              content: Text('Selected: ${reaction.emoji}'),
                              duration: const Duration(milliseconds: 800),
                            ),
                          );
                        }
                      },
                      child: const _TryButton(label: 'Hold'),
                    ),
                  ),
                ],
              ),

              const SizedBox(height: 24),

              // ── Reaction Sets ──
              _SectionTitle(title: 'REACTION SETS', icon: CupertinoIcons.collections),
              _Card(
                color: cardColor,
                children: [
                  _FeatureRow(
                    icon: CupertinoIcons.chat_bubble_2_fill,
                    iconColor: const Color(0xFF007AFF),
                    title: 'Social',
                    subtitle: 'WhatsApp/Telegram style',
                    child: ReactionTarget(
                      reactions: ReactionSet.social.reactions,
                      child: const _TryButton(label: 'Hold'),
                    ),
                  ),
                  _CardDivider(isDark: isDark),
                  _FeatureRow(
                    icon: CupertinoIcons.briefcase_fill,
                    iconColor: const Color(0xFF5856D6),
                    title: 'Work',
                    subtitle: 'Professional reactions',
                    child: ReactionTarget(
                      reactions: ReactionSet.work.reactions,
                      child: const _TryButton(label: 'Hold'),
                    ),
                  ),
                  _CardDivider(isDark: isDark),
                  _FeatureRow(
                    icon: CupertinoIcons.game_controller_solid,
                    iconColor: const Color(0xFFFF3B30),
                    title: 'Gaming',
                    subtitle: 'Gamer reactions',
                    child: ReactionTarget(
                      reactions: ReactionSet.gaming.reactions,
                      child: const _TryButton(label: 'Hold'),
                    ),
                  ),
                  _CardDivider(isDark: isDark),
                  _FeatureRow(
                    icon: CupertinoIcons.minus_circle_fill,
                    iconColor: const Color(0xFF8E8E93),
                    title: 'Minimal',
                    subtitle: 'Just the essentials',
                    child: ReactionTarget(
                      reactions: ReactionSet.minimal.reactions,
                      child: const _TryButton(label: 'Hold'),
                    ),
                  ),
                ],
              ),

              const SizedBox(height: 100),
            ],
          ),
        ),
      ],
    );
  }
}

// ─── Tab 3: Settings ─────────────────────────────────────────────────────────

class _SettingsTab extends StatelessWidget {
  const _SettingsTab();

  @override
  Widget build(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    final cardColor = isDark ? _AppColors.cardDark : _AppColors.cardLight;

    return CustomScrollView(
      slivers: [
        const CupertinoSliverNavigationBar(
          largeTitle: Text('About'),
          border: null,
        ),
        SliverPadding(
          padding: const EdgeInsets.all(16),
          sliver: SliverList.list(
            children: [
              // Package info card
              Container(
                padding: const EdgeInsets.all(20),
                decoration: BoxDecoration(
                  gradient: const LinearGradient(
                    colors: [Color(0xFF007AFF), Color(0xFF5856D6)],
                    begin: Alignment.topLeft,
                    end: Alignment.bottomRight,
                  ),
                  borderRadius: BorderRadius.circular(16),
                ),
                child: Column(
                  children: [
                    Container(
                      width: 64,
                      height: 64,
                      decoration: BoxDecoration(
                        color: Colors.white.withValues(alpha: 0.2),
                        borderRadius: BorderRadius.circular(16),
                      ),
                      child: const Icon(
                        CupertinoIcons.smiley_fill,
                        color: Colors.white,
                        size: 36,
                      ),
                    ),
                    const SizedBox(height: 12),
                    const Text(
                      'Reaction Overlay',
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 22,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 4),
                    Text(
                      'v1.0.0',
                      style: TextStyle(
                        color: Colors.white.withValues(alpha: 0.7),
                        fontSize: 14,
                      ),
                    ),
                    const SizedBox(height: 12),
                    Text(
                      'A beautiful, customizable reaction overlay for Flutter.\nLong press, tap, or double tap to show reactions.',
                      textAlign: TextAlign.center,
                      style: TextStyle(
                        color: Colors.white.withValues(alpha: 0.85),
                        fontSize: 14,
                        height: 1.4,
                      ),
                    ),
                  ],
                ),
              ),

              const SizedBox(height: 24),

              _SectionTitle(title: 'HIGHLIGHTS', icon: CupertinoIcons.checkmark_seal),
              _Card(
                color: cardColor,
                children: [
                  _InfoRow(
                    icon: CupertinoIcons.paintbrush_fill,
                    iconColor: const Color(0xFFFF2D55),
                    title: 'ThemeExtension',
                    subtitle: 'Global styling via ReactionOverlayThemeData',
                    isDark: isDark,
                  ),
                  _CardDivider(isDark: isDark),
                  _InfoRow(
                    icon: CupertinoIcons.slider_horizontal_3,
                    iconColor: const Color(0xFF007AFF),
                    title: 'Controller',
                    subtitle: 'Programmatic show/dismiss/toggle',
                    isDark: isDark,
                  ),
                  _CardDivider(isDark: isDark),
                  _InfoRow(
                    icon: CupertinoIcons.hand_raised_fill,
                    iconColor: const Color(0xFF34C759),
                    title: 'Multiple Triggers',
                    subtitle: 'Long press, tap, double tap, right click',
                    isDark: isDark,
                  ),
                  _CardDivider(isDark: isDark),
                  _InfoRow(
                    icon: CupertinoIcons.arrow_up_arrow_down_circle_fill,
                    iconColor: const Color(0xFFFF9500),
                    title: 'Flexible Positions',
                    subtitle: 'Auto, above, below, left, right',
                    isDark: isDark,
                  ),
                  _CardDivider(isDark: isDark),
                  _InfoRow(
                    icon: CupertinoIcons.heart_circle_fill,
                    iconColor: const Color(0xFFAF52DE),
                    title: 'Reaction Badges',
                    subtitle: 'Display selected reactions with counts',
                    isDark: isDark,
                  ),
                  _CardDivider(isDark: isDark),
                  _InfoRow(
                    icon: CupertinoIcons.eye_fill,
                    iconColor: const Color(0xFF5856D6),
                    title: 'Accessibility',
                    subtitle: 'Screen readers, keyboard navigation',
                    isDark: isDark,
                  ),
                ],
              ),

              const SizedBox(height: 100),
            ],
          ),
        ),
      ],
    );
  }
}

// ─── Shared UI Components ────────────────────────────────────────────────────

class _SectionTitle extends StatelessWidget {
  const _SectionTitle({required this.title, required this.icon});
  final String title;
  final IconData icon;

  @override
  Widget build(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    return Padding(
      padding: const EdgeInsets.only(left: 4, bottom: 8, top: 4),
      child: Row(
        children: [
          Icon(icon, size: 14,
              color: isDark ? _AppColors.subtitleDark : _AppColors.subtitleLight),
          const SizedBox(width: 6),
          Text(
            title,
            style: TextStyle(
              fontSize: 13,
              fontWeight: FontWeight.w500,
              color: isDark ? _AppColors.subtitleDark : _AppColors.subtitleLight,
              letterSpacing: 0.5,
            ),
          ),
        ],
      ),
    );
  }
}

class _Card extends StatelessWidget {
  const _Card({required this.color, required this.children});
  final Color color;
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: color,
        borderRadius: BorderRadius.circular(14),
      ),
      clipBehavior: Clip.antiAlias,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: children,
      ),
    );
  }
}

class _CardDivider extends StatelessWidget {
  const _CardDivider({required this.isDark});
  final bool isDark;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(left: 70),
      child: Container(
        height: 0.5,
        color: isDark ? _AppColors.separatorDark : _AppColors.separatorLight,
      ),
    );
  }
}

class _FeatureRow extends StatelessWidget {
  const _FeatureRow({
    required this.icon,
    required this.iconColor,
    required this.title,
    required this.subtitle,
    required this.child,
  });

  final IconData icon;
  final Color iconColor;
  final String title;
  final String subtitle;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      child: Row(
        children: [
          Container(
            width: 36,
            height: 36,
            decoration: BoxDecoration(
              color: iconColor.withValues(alpha: 0.12),
              borderRadius: BorderRadius.circular(8),
            ),
            child: Icon(icon, color: iconColor, size: 18),
          ),
          const SizedBox(width: 14),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  title,
                  style: TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.w500,
                    color: isDark ? Colors.white : Colors.black,
                  ),
                ),
                Text(
                  subtitle,
                  style: TextStyle(
                    fontSize: 13,
                    color: isDark
                        ? _AppColors.subtitleDark
                        : _AppColors.subtitleLight,
                  ),
                ),
              ],
            ),
          ),
          child,
        ],
      ),
    );
  }
}

class _InfoRow extends StatelessWidget {
  const _InfoRow({
    required this.icon,
    required this.iconColor,
    required this.title,
    required this.subtitle,
    required this.isDark,
  });

  final IconData icon;
  final Color iconColor;
  final String title;
  final String subtitle;
  final bool isDark;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      child: Row(
        children: [
          Container(
            width: 36,
            height: 36,
            decoration: BoxDecoration(
              color: iconColor.withValues(alpha: 0.12),
              borderRadius: BorderRadius.circular(8),
            ),
            child: Icon(icon, color: iconColor, size: 18),
          ),
          const SizedBox(width: 14),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  title,
                  style: TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.w500,
                    color: isDark ? Colors.white : Colors.black,
                  ),
                ),
                Text(
                  subtitle,
                  style: TextStyle(
                    fontSize: 13,
                    color: isDark
                        ? _AppColors.subtitleDark
                        : _AppColors.subtitleLight,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class _TryButton extends StatelessWidget {
  const _TryButton({required this.label});
  final String label;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
      decoration: BoxDecoration(
        color: _AppColors.accent.withValues(alpha: 0.12),
        borderRadius: BorderRadius.circular(20),
      ),
      child: Text(
        label,
        style: const TextStyle(
          color: _AppColors.accent,
          fontSize: 14,
          fontWeight: FontWeight.w600,
        ),
      ),
    );
  }
}
4
likes
160
points
82
downloads

Documentation

API reference

Publisher

verified publisherignacio-manchu.com

Weekly Downloads

A floating reaction bar (like iMessage/WhatsApp) for Flutter. Wrap any widget to add emoji reactions with animations, haptic feedback, dark mode, and smart positioning.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on reaction_overlay