reaction_overlay 0.1.0
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.
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,
),
),
);
}
}