my_animated_widgets 0.0.4
my_animated_widgets: ^0.0.4 copied to clipboard
A collection of beautiful, production-ready animated Flutter widgets — Plasma Globe, Orbital Action Menu, Sky Theme Toggle, Liquid Progress Bar, and Dark Mode Toggle. Each widget is fully customisable [...]
example/lib/main.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:my_animated_widgets/my_animated_widgets.dart';
// ─────────────────────────────────────────────────────────────────────────────
// ENTRY POINT
// ─────────────────────────────────────────────────────────────────────────────
void main() => runApp(const MyApp());
// ─────────────────────────────────────────────────────────────────────────────
// APP ROOT
// ─────────────────────────────────────────────────────────────────────────────
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _isDark = false;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'my_animated_widgets',
debugShowCheckedModeBanner: false,
theme: AppTheme.light(),
darkTheme: AppTheme.dark(),
themeMode: _isDark ? ThemeMode.dark : ThemeMode.light,
home: ShowcasePage(
isDark: _isDark,
onThemeToggle: (v) => setState(() => _isDark = v),
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// THEME
// ─────────────────────────────────────────────────────────────────────────────
class AppTheme {
static const teal = Color(0xFF01696F);
static const tealDark = Color(0xFF014E53);
static const _darkBg = Color(0xFF111210);
static const _darkSurface = Color(0xFF1B1A18);
static ThemeData light() => ThemeData(
useMaterial3: true,
colorSchemeSeed: teal,
brightness: Brightness.light,
scaffoldBackgroundColor: const Color(0xFFF4F3EF),
cardColor: Colors.white,
);
static ThemeData dark() => ThemeData(
useMaterial3: true,
colorSchemeSeed: teal,
brightness: Brightness.dark,
scaffoldBackgroundColor: _darkBg,
cardColor: _darkSurface,
);
}
// ─────────────────────────────────────────────────────────────────────────────
// SHOWCASE PAGE — 5 tabs, one per package widget
// ─────────────────────────────────────────────────────────────────────────────
/// Each entry: (emoji, short label, full title, subtitle, widget builder)
typedef _TabDef = ({
String emoji,
String label,
String title,
String subtitle,
Widget Function(bool isDark, ValueChanged<bool> onToggle) builder,
});
class ShowcasePage extends StatelessWidget {
final bool isDark;
final ValueChanged<bool> onThemeToggle;
const ShowcasePage({
super.key,
required this.isDark,
required this.onThemeToggle,
});
List<_TabDef> get _tabs => [
(
emoji: '⚡',
label: 'Plasma',
title: 'Plasma Globe',
subtitle: 'Interactive arcs that follow your touch',
builder: (_, __) => const _PlasmaTab(),
),
(
emoji: '🌌',
label: 'Orbital',
title: 'Orbital Action Menu',
subtitle: 'Circular FAB with sparkle orbit rings',
builder: (_, __) => const _OrbitalTab(),
),
(
emoji: '🌗',
label: 'Sky',
title: 'Sky Theme Toggle',
subtitle: 'Animated day-to-night sky switch',
builder: (isDark, onToggle) =>
_SkyTab(isDark: isDark, onToggle: onToggle),
),
(
emoji: '⏳',
label: 'Liquid',
title: 'Liquid Progress Bar',
subtitle: 'Wavy liquid-fill animated progress',
builder: (_, __) => const _LiquidTab(),
),
(
emoji: '🌙',
label: 'Toggle',
title: 'Dark Mode Toggle',
subtitle: 'Sun/moon animated theme switch',
builder: (isDark, onToggle) =>
_DarkModeTab(isDark: isDark, onToggle: onToggle),
),
];
@override
Widget build(BuildContext context) {
final tabs = _tabs;
return DefaultTabController(
length: tabs.length,
child: Scaffold(
appBar: _buildAppBar(context, tabs),
body: TabBarView(
physics: const BouncingScrollPhysics(),
children: tabs
.map((t) => t.builder(isDark, onThemeToggle))
.toList(),
),
),
);
}
PreferredSizeWidget _buildAppBar(
BuildContext context, List<_TabDef> tabs) {
final cs = Theme.of(context).colorScheme;
return PreferredSize(
preferredSize: const Size.fromHeight(120),
child: Container(
decoration: BoxDecoration(
color: cs.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 12,
offset: const Offset(0, 3),
),
],
),
child: SafeArea(
child: Column(
children: [
// ── Top row: title + dark toggle ───────
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
// Package label
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color:
cs.primaryContainer.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'pub.dev',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: cs.primary,
letterSpacing: 0.5,
),
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
'my_animated_widgets',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w800,
color: cs.onSurface,
letterSpacing: -0.3,
),
overflow: TextOverflow.ellipsis,
),
),
DarkModeToggle(
isDark: isDark,
onChanged: onThemeToggle,
),
],
),
),
// ── Tab bar ─────────────────────────────
TabBar(
isScrollable: true,
tabAlignment: TabAlignment.start,
indicatorColor: cs.primary,
indicatorWeight: 3,
dividerColor: Colors.transparent,
labelStyle: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
),
unselectedLabelStyle: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
),
tabs: tabs
.map((t) => Tab(text: '${t.emoji} ${t.label}'))
.toList(),
),
],
),
),
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// SHARED: widget page shell
// ─────────────────────────────────────────────────────────────────────────────
/// Wraps every tab: gradient header card + demo area.
class _WidgetPage extends StatelessWidget {
final String emoji;
final String title;
final String subtitle;
final String className;
final List<String> props;
final Color headerColor;
final Color headerColorDark;
final Widget demo;
const _WidgetPage({
required this.emoji,
required this.title,
required this.subtitle,
required this.className,
required this.props,
required this.headerColor,
required this.headerColorDark,
required this.demo,
});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final cs = Theme.of(context).colorScheme;
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── Header card ──────────────────────────
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [headerColorDark, headerColorDark.withValues(alpha: 0.4)]
: [headerColor, headerColor.withValues(alpha: 0.7)],
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(emoji,
style: const TextStyle(fontSize: 36)),
const SizedBox(height: 8),
Text(
title,
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.w800,
letterSpacing: -0.3,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.75),
fontSize: 13,
),
),
const SizedBox(height: 16),
// Class name chip
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.25),
borderRadius: BorderRadius.circular(8),
),
child: Text(
className,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontFamily: 'monospace',
fontWeight: FontWeight.w600,
),
),
),
],
),
),
const SizedBox(height: 20),
// ── Props label ──────────────────────────
Text(
'Key Parameters',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: cs.primary,
letterSpacing: 0.2,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 6,
runSpacing: 6,
children: props
.map((p) => _PropChip(label: p))
.toList(),
),
const SizedBox(height: 24),
// ── Demo label ───────────────────────────
Text(
'Live Demo',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: cs.primary,
letterSpacing: 0.2,
),
),
const SizedBox(height: 12),
// ── Demo widget ──────────────────────────
demo,
],
),
);
}
}
class _PropChip extends StatelessWidget {
final String label;
const _PropChip({required this.label});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4),
decoration: BoxDecoration(
color: cs.primaryContainer.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: cs.primary.withValues(alpha: 0.2)),
),
child: Text(
label,
style: TextStyle(
fontSize: 11,
fontFamily: 'monospace',
fontWeight: FontWeight.w600,
color: cs.primary,
),
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// TAB 1 — PLASMA GLOBE
// ─────────────────────────────────────────────────────────────────────────────
class _PlasmaTab extends StatelessWidget {
const _PlasmaTab();
@override
Widget build(BuildContext context) {
return _WidgetPage(
emoji: '⚡',
title: 'Plasma Globe',
subtitle: 'Plasma arcs that track your finger in real time',
className: 'PlasmaGlobeWidget',
props: const [
'radius',
'primaryColor',
'secondaryColor',
'arcCount',
],
headerColor: const Color(0xFF6D28D9),
headerColorDark: const Color(0xFF3B1278),
demo: const _PlasmaDemo(),
);
}
}
class _PlasmaDemo extends StatefulWidget {
const _PlasmaDemo();
@override
State<_PlasmaDemo> createState() => _PlasmaDemoState();
}
class _PlasmaDemoState extends State<_PlasmaDemo> {
Color _primary = const Color(0xFF8B5CF6);
Color _secondary = const Color(0xFF06B6D4);
double _radius = 140;
int _arcCount = 10;
static const _presets = [
('Violet & Cyan', Color(0xFF8B5CF6), Color(0xFF06B6D4)),
('Pink & Orange', Color(0xFFEC4899), Color(0xFFF97316)),
('Green & Blue', Color(0xFF10B981), Color(0xFF3B82F6)),
('Gold & Red', Color(0xFFD97706), Color(0xFFDC2626)),
];
@override
Widget build(BuildContext context) {
return Column(
children: [
// Globe demo
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Container(
height: 380,
decoration: const BoxDecoration(
gradient: RadialGradient(
center: Alignment(0, 0.2),
radius: 1.1,
colors: [Color(0xFF1A0A30), Color(0xFF060618)],
),
),
child: Stack(
children: [
const _StarFieldBg(),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
PlasmaGlobeWidget(
radius: _radius,
primaryColor: _primary,
secondaryColor: _secondary,
arcCount: _arcCount,
),
const SizedBox(height: 20),
Text(
't o u c h m e',
style: TextStyle(
color: _primary.withValues(alpha: 0.7),
fontSize: 10,
letterSpacing: 6,
),
),
],
),
),
],
),
),
),
const SizedBox(height: 20),
// Controls
_ControlCard(
children: [
_SliderRow(
label: 'Radius',
value: _radius,
min: 80,
max: 180,
onChanged: (v) => setState(() => _radius = v),
display: _radius.round().toString(),
),
_SliderRow(
label: 'Arc Count',
value: _arcCount.toDouble(),
min: 4,
max: 16,
divisions: 12,
onChanged: (v) => setState(() => _arcCount = v.round()),
display: _arcCount.toString(),
),
const SizedBox(height: 8),
const Text('Color Preset',
style: TextStyle(
fontSize: 12, fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _presets.map((p) {
final active =
_primary == p.$2 && _secondary == p.$3;
return GestureDetector(
onTap: () => setState(() {
_primary = p.$2;
_secondary = p.$3;
}),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
gradient: LinearGradient(
colors: [p.$2, p.$3]),
boxShadow: active
? [
BoxShadow(
color: p.$2.withValues(alpha: 0.4),
blurRadius: 8)
]
: [],
),
child: Text(
p.$1,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
),
);
}).toList(),
),
],
),
],
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// TAB 2 — ORBITAL ACTION MENU
// ─────────────────────────────────────────────────────────────────────────────
class _OrbitalTab extends StatelessWidget {
const _OrbitalTab();
@override
Widget build(BuildContext context) {
return _WidgetPage(
emoji: '🌌',
title: 'Orbital Action Menu',
subtitle: 'Radial FAB with animated orbit rings and sparkles',
className: 'OrbitalActionMenu',
props: const [
'icon',
'color',
'actions',
'orbitRadius',
'buttonSize',
],
headerColor: const Color(0xFF4C1D95),
headerColorDark: const Color(0xFF2D1268),
demo: const _OrbitalDemo(),
);
}
}
class _OrbitalDemo extends StatefulWidget {
const _OrbitalDemo();
@override
State<_OrbitalDemo> createState() => _OrbitalDemoState();
}
class _OrbitalDemoState extends State<_OrbitalDemo> {
Color _color = const Color(0xFF7C4DFF);
double _orbitRadius = 110;
double _buttonSize = 64;
static const _colorOptions = [
('Purple', Color(0xFF7C4DFF)),
('Teal', Color(0xFF01696F)),
('Pink', Color(0xFFEC4899)),
('Amber', Color(0xFFD97706)),
];
@override
Widget build(BuildContext context) {
return Column(
children: [
// Orbital demo area
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Container(
height: 400,
decoration: const BoxDecoration(
gradient: RadialGradient(
center: Alignment.center,
radius: 1.0,
colors: [Color(0xFF1A1A2E), Color(0xFF0D0D14)],
),
),
child: Center(
child: OrbitalActionMenu(
icon: Icons.auto_awesome,
color: _color,
orbitRadius: _orbitRadius,
buttonSize: _buttonSize,
actions: [
OrbitalAction(
icon: Icons.receipt_long_rounded,
label: 'Invoice',
color: const Color(0xFF00BCD4),
onTap: () {},
),
OrbitalAction(
icon: Icons.person_add_rounded,
label: 'Client',
color: const Color(0xFF69F0AE),
onTap: () {},
),
OrbitalAction(
icon: Icons.qr_code_scanner_rounded,
label: 'Scan',
color: const Color(0xFFFFD740),
onTap: () {},
),
OrbitalAction(
icon: Icons.bar_chart_rounded,
label: 'Reports',
color: const Color(0xFFFF6D9F),
onTap: () {},
),
],
),
),
),
),
const SizedBox(height: 20),
// Controls
_ControlCard(
children: [
_SliderRow(
label: 'Orbit Radius',
value: _orbitRadius,
min: 70,
max: 150,
onChanged: (v) => setState(() => _orbitRadius = v),
display: _orbitRadius.round().toString(),
),
_SliderRow(
label: 'Button Size',
value: _buttonSize,
min: 44,
max: 88,
onChanged: (v) => setState(() => _buttonSize = v),
display: _buttonSize.round().toString(),
),
const SizedBox(height: 8),
const Text('Accent Color',
style: TextStyle(
fontSize: 12, fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
Row(
children: _colorOptions.map((opt) {
final active = _color == opt.$2;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: GestureDetector(
onTap: () =>
setState(() => _color = opt.$2),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 36,
height: 36,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: opt.$2,
border: Border.all(
color: active
? Colors.white
: Colors.transparent,
width: 2.5,
),
boxShadow: active
? [
BoxShadow(
color: opt.$2.withValues(alpha: 0.5),
blurRadius: 10,
)
]
: [],
),
),
),
);
}).toList(),
),
],
),
],
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// TAB 3 — SKY THEME TOGGLE
// ─────────────────────────────────────────────────────────────────────────────
class _SkyTab extends StatelessWidget {
final bool isDark;
final ValueChanged<bool> onToggle;
const _SkyTab({required this.isDark, required this.onToggle});
@override
Widget build(BuildContext context) {
return _WidgetPage(
emoji: '🌗',
title: 'Sky Theme Toggle',
subtitle: 'Day-to-night sky transition with shooting stars',
className: 'SkyThemeToggle',
props: const ['isDark', 'onChanged', 'width'],
headerColor: isDark ? const Color(0xFF1A1A4E) : const Color(0xFF0284C7),
headerColorDark:
isDark ? const Color(0xFF0D0D2A) : const Color(0xFF014E53),
demo: _SkyDemo(isDark: isDark, onToggle: onToggle),
);
}
}
class _SkyDemo extends StatefulWidget {
final bool isDark;
final ValueChanged<bool> onToggle;
const _SkyDemo({required this.isDark, required this.onToggle});
@override
State<_SkyDemo> createState() => _SkyDemoState();
}
class _SkyDemoState extends State<_SkyDemo> {
double _width = 220;
@override
Widget build(BuildContext context) {
final isDark = widget.isDark;
return Column(
children: [
// Sky ambient background card
AnimatedContainer(
duration: const Duration(milliseconds: 700),
curve: Curves.easeInOut,
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 48),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: isDark
? [const Color(0xFF060618), const Color(0xFF0A0A1A)]
: [const Color(0xFF5BBEE8), const Color(0xFFB0E0FF)],
),
),
child: Column(
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
child: Text(
isDark ? '🌙 Good Night' : '☀️ Good Morning',
key: ValueKey(isDark),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color:
isDark ? Colors.white70 : const Color(0xFF1A3A4A),
),
),
),
const SizedBox(height: 32),
SkyThemeToggle(
isDark: isDark,
onChanged: widget.onToggle,
width: _width,
),
const SizedBox(height: 32),
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 400),
style: TextStyle(
fontSize: 12,
color:
isDark ? Colors.white30 : const Color(0xFF3A6A7A),
letterSpacing: 1,
),
child: Text(
isDark ? 'NIGHT MODE ACTIVE' : 'DAY MODE ACTIVE',
),
),
],
),
),
const SizedBox(height: 20),
_ControlCard(
children: [
_SliderRow(
label: 'Width',
value: _width,
min: 140,
max: 300,
onChanged: (v) => setState(() => _width = v),
display: _width.round().toString(),
),
],
),
],
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// TAB 4 — LIQUID PROGRESS BAR
// ─────────────────────────────────────────────────────────────────────────────
class _LiquidTab extends StatelessWidget {
const _LiquidTab();
@override
Widget build(BuildContext context) {
return _WidgetPage(
emoji: '⏳',
title: 'Liquid Progress Bar',
subtitle: 'Animated wavy liquid fill with percentage label',
className: 'LiquidProgressBar',
props: const ['progress', 'color', 'height'],
headerColor: const Color(0xFF01696F),
headerColorDark: const Color(0xFF014E53),
demo: const _LiquidDemo(),
);
}
}
class _LiquidDemo extends StatefulWidget {
const _LiquidDemo();
@override
State<_LiquidDemo> createState() => _LiquidDemoState();
}
class _LiquidDemoState extends State<_LiquidDemo> {
double _progress = 0.6;
double _height = 28;
static const _bars = [
('Monthly Target', Color(0xFF01696F)),
('Weekly Target', Color(0xFF964219)),
('Daily Goal', Color(0xFF4C1D95)),
];
@override
Widget build(BuildContext context) {
return Column(
children: [
_ControlCard(
children: [
// Live bars
...List.generate(_bars.length, (i) {
final factor = (1 - i * 0.2).clamp(0.0, 1.0);
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(_bars[i].$1,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600)),
Text(
'${(_progress * factor * 100).round()}%',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: _bars[i].$2,
),
),
],
),
const SizedBox(height: 8),
LiquidProgressBar(
progress: _progress * factor,
color: _bars[i].$2,
height: _height,
),
],
),
);
}),
const Divider(height: 24),
// Controls
_SliderRow(
label: 'Progress',
value: _progress,
min: 0,
max: 1,
onChanged: (v) => setState(() => _progress = v),
display: '${(_progress * 100).round()}%',
),
_SliderRow(
label: 'Height',
value: _height,
min: 16,
max: 48,
onChanged: (v) => setState(() => _height = v),
display: _height.round().toString(),
),
],
),
],
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// TAB 5 — DARK MODE TOGGLE
// ─────────────────────────────────────────────────────────────────────────────
class _DarkModeTab extends StatelessWidget {
final bool isDark;
final ValueChanged<bool> onToggle;
const _DarkModeTab({required this.isDark, required this.onToggle});
@override
Widget build(BuildContext context) {
return _WidgetPage(
emoji: '🌙',
title: 'Dark Mode Toggle',
subtitle: 'Elastic sun/moon animated switch',
className: 'DarkModeToggle',
props: const ['isDark', 'onChanged', 'animationDuration'],
headerColor: isDark ? const Color(0xFF1E1B4B) : const Color(0xFF374151),
headerColorDark: const Color(0xFF111827),
demo: _DarkModeDemo(isDark: isDark, onToggle: onToggle),
);
}
}
class _DarkModeDemo extends StatefulWidget {
final bool isDark;
final ValueChanged<bool> onToggle;
const _DarkModeDemo({required this.isDark, required this.onToggle});
@override
State<_DarkModeDemo> createState() => _DarkModeDemoState();
}
class _DarkModeDemoState extends State<_DarkModeDemo> {
Duration _duration = const Duration(milliseconds: 400);
static const _durations = [
('Snappy', Duration(milliseconds: 200)),
('Normal', Duration(milliseconds: 400)),
('Slow', Duration(milliseconds: 900)),
];
@override
Widget build(BuildContext context) {
final isDark = widget.isDark;
return Column(
children: [
// Demo card
AnimatedContainer(
duration: const Duration(milliseconds: 600),
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 40),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: isDark
? const Color(0xFF111827)
: const Color(0xFFF9FAFB),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.07)
: Colors.black.withValues(alpha: 0.07),
),
),
child: Column(
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
child: Text(
isDark ? '🌙 Dark Mode' : '☀️ Light Mode',
key: ValueKey(isDark),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: isDark ? Colors.white : Colors.black87,
),
),
),
const SizedBox(height: 28),
DarkModeToggle(
isDark: isDark,
onChanged: widget.onToggle,
animationDuration: _duration,
),
const SizedBox(height: 16),
Text(
isDark ? 'Tap to switch to Light' : 'Tap to switch to Dark',
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.white38 : Colors.black38,
),
),
],
),
),
const SizedBox(height: 20),
// Controls
_ControlCard(
children: [
const Text(
'Animation Speed',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
),
const SizedBox(height: 10),
Row(
children: _durations.map((d) {
final active = _duration == d.$2;
final cs = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: GestureDetector(
onTap: () =>
setState(() => _duration = d.$2),
child: AnimatedContainer(
duration:
const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: active
? cs.primary
: cs.primaryContainer
.withValues(alpha: 0.4),
borderRadius:
BorderRadius.circular(10),
),
child: Text(
d.$1,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: active
? Colors.white
: cs.primary,
),
),
),
),
);
}).toList(),
),
],
),
],
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// SHARED CONTROL WIDGETS
// ─────────────────────────────────────────────────────────────────────────────
class _ControlCard extends StatelessWidget {
final List<Widget> children;
const _ControlCard({required this.children});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.06)
: Colors.black.withValues(alpha: 0.05),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.06),
blurRadius: 14,
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
);
}
}
class _SliderRow extends StatelessWidget {
final String label;
final double value;
final double min;
final double max;
final int? divisions;
final ValueChanged<double> onChanged;
final String display;
const _SliderRow({
required this.label,
required this.value,
required this.min,
required this.max,
required this.onChanged,
required this.display,
this.divisions,
});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label,
style: const TextStyle(
fontSize: 12, fontWeight: FontWeight.w600)),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: cs.primaryContainer.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(6),
),
child: Text(
display,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: cs.primary,
),
),
),
],
),
SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: cs.primary,
thumbColor: cs.primary,
overlayColor: cs.primary.withValues(alpha: 0.12),
inactiveTrackColor: cs.primary.withValues(alpha: 0.18),
trackHeight: 3,
),
child: Slider(
value: value,
min: min,
max: max,
divisions: divisions,
onChanged: onChanged,
),
),
],
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// STAR FIELD BACKGROUND (for Plasma tab)
// ─────────────────────────────────────────────────────────────────────────────
class _StarFieldBg extends StatelessWidget {
const _StarFieldBg();
@override
Widget build(BuildContext context) {
return SizedBox.expand(
child: CustomPaint(painter: _StarFieldPainter(Random(99))),
);
}
}
class _StarFieldPainter extends CustomPainter {
final Random rng;
_StarFieldPainter(this.rng);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.white.withValues(alpha: 0.3);
for (int i = 0; i < 55; i++) {
canvas.drawCircle(
Offset(rng.nextDouble() * size.width,
rng.nextDouble() * size.height),
rng.nextDouble() * 1.1 + 0.3,
paint,
);
}
}
@override
bool shouldRepaint(_StarFieldPainter old) => false;
}