expressive_kit 0.0.8
expressive_kit: ^0.0.8 copied to clipboard
A modern Flutter design system providing Material 3 Expressive components, spring motions, tactile feedback, and unified theme styles.
import 'package:expressive_kit/expressive_kit.dart';
import 'package:hugeicons/hugeicons.dart';
import 'package:material_ui/material_ui.dart';
void main() {
runApp(const ExampleApp());
}
class ExampleApp extends StatefulWidget {
const ExampleApp({super.key});
@override
State<ExampleApp> createState() => _ExampleAppState();
}
class _ExampleAppState extends State<ExampleApp> {
bool _isDarkMode = false;
String _selectedFontFamily = ExpressiveFonts.googleSansFlex;
void _toggleTheme() {
setState(() => _isDarkMode = !_isDarkMode);
}
void _changeFont(String newFont) {
setState(() => _selectedFontFamily = newFont);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Expressive Kit Gallery',
debugShowCheckedModeBanner: false,
theme: ExpressiveTheme.light(
baseColor: const Color(0xFF1E88E5),
fontFamily: _selectedFontFamily,
),
darkTheme: ExpressiveTheme.dark(
baseColor: const Color(0xFF1E88E5),
fontFamily: _selectedFontFamily,
),
themeMode: _isDarkMode ? ThemeMode.dark : ThemeMode.light,
home: GalleryScreen(
isDarkMode: _isDarkMode,
onToggleTheme: _toggleTheme,
selectedFont: _selectedFontFamily,
onFontChanged: _changeFont,
),
);
}
}
class GalleryScreen extends StatefulWidget {
const GalleryScreen({
required this.isDarkMode,
required this.onToggleTheme,
required this.selectedFont,
required this.onFontChanged,
super.key,
});
final bool isDarkMode;
final VoidCallback onToggleTheme;
final String selectedFont;
final ValueChanged<String> onFontChanged;
@override
State<GalleryScreen> createState() => _GalleryScreenState();
}
class _GalleryScreenState extends State<GalleryScreen> {
int _selectedTab = 0;
// Controls state
bool _btnLoading = false;
String _selectedFrequency = 'Weekly';
bool _switchVal1 = true;
bool _switchVal2 = false;
double _sliderVal = 65.0;
double _sliderStepVal = 3.0;
int _badgeCount = 3;
// Motion playground state
double _scaleBouncy = 1.0;
double _scaleGentle = 1.0;
double _scaleSnappy = 1.0;
// Typography test text controller
final TextEditingController _customTextController = TextEditingController(
text: 'The quick brown fox jumps over the lazy dog.',
);
@override
void dispose() {
_customTextController.dispose();
super.dispose();
}
void _showFeedback(String message) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(message)));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: ExpressiveTopAppBar(
title: const Text('Expressive Kit Gallery'),
actions: [
ExpressiveIconButton.circle(
icon: HugeIcon(
icon: widget.isDarkMode
? HugeIcons.strokeRoundedSun03
: HugeIcons.strokeRoundedMoon02,
color: Theme.of(context).colorScheme.onSurface,
),
tooltip: widget.isDarkMode ? 'Light mode' : 'Dark mode',
onPressed: widget.onToggleTheme,
),
ExpressiveIconButton.circle(
icon: const HugeIcon(icon: HugeIcons.strokeRoundedNotification01),
badgeText: '$_badgeCount',
tooltip: 'Notifications',
onPressed: () {
setState(() => _badgeCount = (_badgeCount + 1) % 10);
_showFeedback('Notifications: $_badgeCount');
},
),
const SizedBox(width: 8),
],
),
body: IndexedStack(
index: _selectedTab,
children: [
_buildControlsTab(),
_buildContentTab(),
_buildProgressAndMotionTab(),
_buildNavigationAndFabTab(),
_buildTypographyTab(),
],
),
bottomNavigationBar: ExpressiveNavigationBar(
selectedIndex: _selectedTab,
onDestinationSelected: (index) {
setState(() => _selectedTab = index);
},
destinations: const [
ExpressiveNavigationDestination(
icon: HugeIcon(icon: HugeIcons.strokeRoundedCursorPointer01),
selectedIcon: HugeIcon(
icon: HugeIcons.strokeRoundedCursorPointer01,
),
label: 'Controls',
),
ExpressiveNavigationDestination(
icon: HugeIcon(icon: HugeIcons.strokeRoundedGridTable),
selectedIcon: HugeIcon(
icon: HugeIcons.strokeRoundedDashboardSquare01,
),
label: 'Content',
),
ExpressiveNavigationDestination(
icon: HugeIcon(icon: HugeIcons.strokeRoundedDashboardSpeed01),
selectedIcon: HugeIcon(
icon: HugeIcons.strokeRoundedDashboardSpeed01,
),
label: 'Progress',
),
ExpressiveNavigationDestination(
icon: HugeIcon(icon: HugeIcons.strokeRoundedNavigation03),
selectedIcon: HugeIcon(icon: HugeIcons.strokeRoundedNavigation03),
label: 'Navigation',
showBadgeDot: true,
),
ExpressiveNavigationDestination(
icon: HugeIcon(icon: HugeIcons.strokeRoundedTextFont),
selectedIcon: HugeIcon(icon: HugeIcons.strokeRoundedTextFont),
label: 'Fonts',
),
],
),
floatingActionButton: ExpressiveFabMenu(
items: [
ExpressiveFabMenuItem(
icon: const HugeIcon(icon: HugeIcons.strokeRoundedShare01),
label: 'Share',
onTap: () => _showFeedback('Action: Share'),
),
ExpressiveFabMenuItem(
icon: const HugeIcon(icon: HugeIcons.strokeRoundedBookmark02),
label: 'Save',
onTap: () => _showFeedback('Action: Save'),
),
ExpressiveFabMenuItem(
icon: const HugeIcon(icon: HugeIcons.strokeRoundedAdd01),
label: 'New item',
onTap: () => _showFeedback('Action: New item'),
),
],
),
);
}
// ---------------------------------------------------------------------------
// TAB 1: BUTTONS AND CONTROLS
// ---------------------------------------------------------------------------
Widget _buildControlsTab() {
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
_buildSectionHeader(
'Buttons (ExpressiveButton)',
'Material 3 variants with spring curves and responsive interactive states.',
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ExpressiveButton(
label: 'Filled',
variant: ExpressiveButtonVariant.filled,
onPressed: () => _showFeedback('Filled button pressed'),
),
ExpressiveButton(
label: 'Tonal',
variant: ExpressiveButtonVariant.tonal,
onPressed: () => _showFeedback('Tonal button pressed'),
),
ExpressiveButton(
label: 'Outlined',
variant: ExpressiveButtonVariant.outlined,
onPressed: () => _showFeedback('Outlined button pressed'),
),
ExpressiveButton(
label: 'Elevated',
variant: ExpressiveButtonVariant.elevated,
onPressed: () => _showFeedback('Elevated button pressed'),
),
ExpressiveButton(
label: 'Text',
variant: ExpressiveButtonVariant.text,
onPressed: () => _showFeedback('Text button pressed'),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ExpressiveButton(
label: 'With Icon',
icon: const HugeIcon(icon: HugeIcons.strokeRoundedStar, size: 20),
variant: ExpressiveButtonVariant.filled,
onPressed: () => _showFeedback('Button with icon pressed'),
),
ExpressiveButton(
label: 'Next',
trailingIcon: const HugeIcon(
icon: HugeIcons.strokeRoundedArrowRight01,
size: 20,
),
variant: ExpressiveButtonVariant.tonal,
onPressed: () => _showFeedback('Next button pressed'),
),
ExpressiveButton(
label: _btnLoading ? 'Loading...' : 'Load Data',
isLoading: _btnLoading,
variant: ExpressiveButtonVariant.outlined,
onPressed: () {
setState(() => _btnLoading = true);
Future.delayed(const Duration(seconds: 2), () {
if (mounted) setState(() => _btnLoading = false);
});
},
),
const ExpressiveButton(label: 'Disabled', onPressed: null),
],
),
const SizedBox(height: 12),
ExpressiveButton(
label: 'Expanded Button (Full Width)',
isExpanded: true,
icon: const HugeIcon(
icon: HugeIcons.strokeRoundedCheckmarkCircle01,
size: 20,
),
variant: ExpressiveButtonVariant.filled,
onPressed: () => _showFeedback('Expanded button pressed'),
),
const SizedBox(height: 24),
_buildSectionHeader(
'Split Button (ExpressiveSplitButton)',
'Continuous primary action with an expandable secondary dropdown.',
),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
ExpressiveSplitButton(
label: 'Save File',
leadingIcon: const HugeIcon(
icon: HugeIcons.strokeRoundedFloppyDisk,
size: 20,
),
variant: ExpressiveButtonVariant.filled,
onPressed: () => _showFeedback('Save file'),
onTrailingPressed: () => _showFeedback('More save options'),
),
ExpressiveSplitButton(
label: 'Export Data',
variant: ExpressiveButtonVariant.tonal,
onPressed: () => _showFeedback('Export data'),
onTrailingPressed: () => _showFeedback('Export format'),
),
ExpressiveSplitButton(
label: 'Send',
variant: ExpressiveButtonVariant.outlined,
onPressed: () => _showFeedback('Send'),
onTrailingPressed: () => _showFeedback('Schedule send'),
),
],
),
const SizedBox(height: 24),
_buildSectionHeader(
'Segmented Picker (ExpressiveButtonGroup)',
'Button group with single selection and continuous sliding pill indicator.',
),
const SizedBox(height: 12),
ExpressiveButtonGroup<String>(
selectedValue: _selectedFrequency,
onSelected: (val) {
setState(() => _selectedFrequency = val);
_showFeedback('Selected frequency: $val');
},
items: const [
ExpressiveButtonGroupItem(
value: 'Daily',
label: 'Daily',
icon: HugeIcon(icon: HugeIcons.strokeRoundedCalendar01, size: 18),
),
ExpressiveButtonGroupItem(
value: 'Weekly',
label: 'Weekly',
icon: HugeIcon(icon: HugeIcons.strokeRoundedCalendar02, size: 18),
),
ExpressiveButtonGroupItem(
value: 'Monthly',
label: 'Monthly',
icon: HugeIcon(icon: HugeIcons.strokeRoundedCalendar03, size: 18),
),
],
),
const SizedBox(height: 24),
_buildSectionHeader(
'Icon Buttons (ExpressiveIconButton)',
'Shapes: circular, squircle, and rounded rectangle, with badge options.',
),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
ExpressiveIconButton.circle(
icon: const HugeIcon(icon: HugeIcons.strokeRoundedFavourite),
variant: ExpressiveIconButtonVariant.filled,
tooltip: 'Favorite (Filled Circle)',
onPressed: () => _showFeedback('Favorite pressed'),
),
ExpressiveIconButton.circle(
icon: const HugeIcon(icon: HugeIcons.strokeRoundedShare01),
variant: ExpressiveIconButtonVariant.tonal,
tooltip: 'Share (Tonal Circle)',
onPressed: () => _showFeedback('Share pressed'),
),
ExpressiveIconButton.circle(
icon: const HugeIcon(icon: HugeIcons.strokeRoundedBookmark02),
variant: ExpressiveIconButtonVariant.outlined,
tooltip: 'Bookmark (Outlined Circle)',
onPressed: () => _showFeedback('Bookmark pressed'),
),
ExpressiveIconButton.squircle(
icon: const HugeIcon(icon: HugeIcons.strokeRoundedCamera01),
variant: ExpressiveIconButtonVariant.filled,
tooltip: 'Camera (Squircle Filled)',
onPressed: () => _showFeedback('Camera pressed'),
),
ExpressiveIconButton.squircle(
icon: const HugeIcon(icon: HugeIcons.strokeRoundedImage01),
variant: ExpressiveIconButtonVariant.tonal,
tooltip: 'Gallery (Squircle Tonal)',
onPressed: () => _showFeedback('Gallery pressed'),
),
ExpressiveIconButton(
icon: const HugeIcon(
icon: HugeIcons.strokeRoundedSlidersHorizontal,
),
shape: ExpressiveIconButtonShape.roundedRectangle,
variant: ExpressiveIconButtonVariant.filled,
tooltip: 'Settings (Rounded Rect)',
onPressed: () => _showFeedback('Settings pressed'),
),
ExpressiveIconButton.circle(
icon: const HugeIcon(icon: HugeIcons.strokeRoundedMail01),
variant: ExpressiveIconButtonVariant.tonal,
badgeText: '9+',
tooltip: 'Messages with Badge',
onPressed: () => _showFeedback('Messages: 9+ unread'),
),
ExpressiveIconButton.circle(
icon: const HugeIcon(icon: HugeIcons.strokeRoundedNotification01),
showBadgeDot: true,
tooltip: 'Alert with Dot',
onPressed: () => _showFeedback('Active alert'),
),
],
),
const SizedBox(height: 24),
_buildSectionHeader(
'Switches (ExpressiveSwitch)',
'Capsule track with check/cross icons in the thumb and spring physics.',
),
const SizedBox(height: 12),
Row(
children: [
ExpressiveSwitch(
value: _switchVal1,
onChanged: (val) => setState(() => _switchVal1 = val),
),
const SizedBox(width: 12),
Text(
_switchVal1 ? 'Enabled (With icons)' : 'Disabled (With icons)',
style: const TextStyle(fontWeight: FontWeight.w500),
),
],
),
const SizedBox(height: 12),
Row(
children: [
ExpressiveSwitch(
value: _switchVal2,
showIcons: false,
onChanged: (val) => setState(() => _switchVal2 = val),
),
const SizedBox(width: 12),
Text(
_switchVal2 ? 'Enabled (No icons)' : 'Disabled (No icons)',
style: const TextStyle(fontWeight: FontWeight.w500),
),
],
),
const SizedBox(height: 24),
_buildSectionHeader(
'Sliders (ExpressiveSlider)',
'Thick capsule track with pill thumb indicator and stepper buttons (+/-).',
),
const SizedBox(height: 8),
ExpressiveSlider(
value: _sliderVal,
min: 0,
max: 100,
title: 'Media volume',
subtitle: 'Current level: ${_sliderVal.toInt()}%',
onChanged: (val) => setState(() => _sliderVal = val),
),
const SizedBox(height: 12),
ExpressiveSlider(
value: _sliderStepVal,
min: 1,
max: 5,
divisions: 4,
title: 'Graphics quality (Discrete)',
subtitle: 'Level: ${_sliderStepVal.toInt()} of 5 stars',
onChanged: (val) => setState(() => _sliderStepVal = val),
),
const SizedBox(height: 80),
],
);
}
// ---------------------------------------------------------------------------
// TAB 2: CONTENT, CARDS AND LISTS
// ---------------------------------------------------------------------------
Widget _buildContentTab() {
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
_buildSectionHeader(
'Search Bar (ExpressiveSearchBar)',
'28dp pill container with search icon and embedded trailing actions.',
),
const SizedBox(height: 12),
ExpressiveSearchBar(
hintText: 'Search components, colors, or settings...',
margin: EdgeInsets.zero,
trailing: ExpressiveIconButton.circle(
icon: const HugeIcon(icon: HugeIcons.strokeRoundedMic01, size: 20),
tooltip: 'Voice search',
onPressed: () => _showFeedback('Voice search activated'),
),
onSubmitted: (query) => _showFeedback('Searching: "$query"'),
),
const SizedBox(height: 12),
ExpressiveSearchBar(
hintText: 'Read-only mode (Tap to explore)',
readOnly: true,
margin: EdgeInsets.zero,
trailing: const HugeIcon(
icon: HugeIcons.strokeRoundedArrowRight01,
size: 16,
),
onTap: () => _showFeedback('Search modal opened'),
),
const SizedBox(height: 24),
_buildSectionHeader(
'Multi-Browse Carousel (ExpressiveCarousel)',
'Smooth horizontal scrolling with 24dp adaptive corners and spring physics.',
),
const SizedBox(height: 12),
ExpressiveCarousel(
height: 160,
itemExtent: 220,
items: [
ExpressiveCarouselItem(
onTap: () => _showFeedback('Card 1 selected'),
child: Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF1E88E5), Color(0xFF42A5F5)],
),
borderRadius: BorderRadius.circular(24),
),
padding: const EdgeInsets.all(16),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
HugeIcon(
icon: HugeIcons.strokeRoundedPaintBoard,
color: Colors.white,
size: 32,
),
Text(
'Material 3\nExpressive Theme',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
),
),
ExpressiveCarouselItem(
onTap: () => _showFeedback('Card 2 selected'),
child: Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF7B1FA2), Color(0xFFBA68C8)],
),
borderRadius: BorderRadius.circular(24),
),
padding: const EdgeInsets.all(16),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
HugeIcon(
icon: HugeIcons.strokeRoundedMagicWand01,
color: Colors.white,
size: 32,
),
Text(
'Spring Bouncy\nPhysics Motion',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
),
),
ExpressiveCarouselItem(
onTap: () => _showFeedback('Card 3 selected'),
child: Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF00897B), Color(0xFF4DB6AC)],
),
borderRadius: BorderRadius.circular(24),
),
padding: const EdgeInsets.all(16),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
HugeIcon(
icon: HugeIcons.strokeRoundedCube,
color: Colors.white,
size: 32,
),
Text(
'15 Native\nComponents & Tests',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
),
),
],
),
const SizedBox(height: 24),
_buildSectionHeader(
'Card Group & List Tiles (ExpressiveCardGroup & ExpressiveListTile)',
'Pixel Settings list style with vibrant circular badges and automatic dividers.',
),
const SizedBox(height: 12),
ExpressiveCardGroup(
title: 'Connectivity Preferences',
margin: EdgeInsets.zero,
children: [
ExpressiveListTile(
leading: const ExpressiveBadge(
icon: HugeIcon(
icon: HugeIcons.strokeRoundedWifi01,
color: Colors.white,
size: 20,
),
backgroundColor: Color(0xFF1E88E5),
foregroundColor: Colors.white,
),
title: const Text('Internet & network'),
subtitle: const Text('Home Wi-Fi 5GHz connected'),
trailing: const HugeIcon(
icon: HugeIcons.strokeRoundedArrowRight01,
size: 20,
),
onTap: () => _showFeedback('Wi-Fi settings'),
),
ExpressiveListTile(
leading: const ExpressiveBadge(
icon: HugeIcon(
icon: HugeIcons.strokeRoundedBluetooth,
color: Colors.white,
size: 20,
),
backgroundColor: Color(0xFF5E35B1),
foregroundColor: Colors.white,
),
title: const Text('Connected devices'),
subtitle: const Text('Pixel Buds Pro headphones'),
trailing: const HugeIcon(
icon: HugeIcons.strokeRoundedArrowRight01,
size: 20,
),
onTap: () => _showFeedback('Bluetooth settings'),
),
ExpressiveListTile(
leading: const ExpressiveBadge(
icon: HugeIcon(
icon: HugeIcons.strokeRoundedShield01,
color: Colors.white,
size: 20,
),
backgroundColor: Color(0xFF2E7D32),
foregroundColor: Colors.white,
),
title: const Text('Security & privacy'),
subtitle: const Text('Device protected and up to date'),
trailing: const HugeIcon(
icon: HugeIcons.strokeRoundedArrowRight01,
size: 20,
),
onTap: () => _showFeedback('Security settings'),
),
ExpressiveListTile(
leading: const ExpressiveBadge(
icon: HugeIcon(
icon: HugeIcons.strokeRoundedMoon02,
color: Colors.white,
size: 20,
),
backgroundColor: Color(0xFFEF6C00),
foregroundColor: Colors.white,
),
title: const Text('Dark theme'),
subtitle: const Text('Toggle visual appearance'),
showSplitDivider: true,
onTap: widget.onToggleTheme,
secondaryTrailing: ExpressiveSwitch(
value: widget.isDarkMode,
onChanged: (_) => widget.onToggleTheme(),
),
),
],
),
const SizedBox(height: 24),
_buildSectionHeader(
'Standalone Card (ExpressiveCard)',
'Elevated surface with smooth 24dp corners and responsive touch reaction.',
),
const SizedBox(height: 12),
ExpressiveCard(
margin: EdgeInsets.zero,
onTap: () => _showFeedback('Standalone card pressed'),
child: Row(
children: [
const ExpressiveBadge(
icon: HugeIcon(
icon: HugeIcons.strokeRoundedStar,
color: Colors.black87,
size: 20,
),
backgroundColor: Colors.amber,
foregroundColor: Colors.black87,
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Expressive Experience',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
SizedBox(height: 4),
Text(
'Modern design language inspired by Material Design 3 Expressive guidelines.',
style: TextStyle(fontSize: 13),
),
],
),
),
const HugeIcon(
icon: HugeIcons.strokeRoundedArrowRight01,
size: 20,
),
],
),
),
const SizedBox(height: 80),
],
);
}
// ---------------------------------------------------------------------------
// TAB 3: PROGRESS AND MOTION TOKENS
// ---------------------------------------------------------------------------
Widget _buildProgressAndMotionTab() {
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
_buildSectionHeader(
'Material 3 Expressive Progress Indicators',
'Wavy progress indicators in Determinate (Column 1) and Indeterminate (Column 2) modes.',
),
const SizedBox(height: 16),
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 760),
child: Card(
elevation: 0,
color: Theme.of(context).colorScheme.surfaceContainerLowest,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
side: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant
.withValues(alpha: 0.5),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 28,
vertical: 36,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Column 1: Determinate
Expanded(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ExpressiveLinearProgressIndicator(
value: _sliderVal / 100,
),
),
const SizedBox(height: 36),
ExpressiveCircularProgressIndicator(
value: _sliderVal / 100,
),
const SizedBox(height: 28),
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSurface,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(
'1',
style: TextStyle(
color: Theme.of(context).colorScheme.surface,
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
),
const SizedBox(height: 8),
Text(
'Determinate (${_sliderVal.toInt()}%)',
style: Theme.of(context).textTheme.labelMedium
?.copyWith(fontWeight: FontWeight.w600),
),
],
),
),
const SizedBox(width: 24),
// Column 2: Indeterminate
Expanded(
child: Column(
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: ExpressiveLinearProgressIndicator(),
),
const SizedBox(height: 36),
const ExpressiveCircularProgressIndicator(),
const SizedBox(height: 28),
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSurface,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(
'2',
style: TextStyle(
color: Theme.of(context).colorScheme.surface,
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
),
const SizedBox(height: 8),
Text(
'Indeterminate',
style: Theme.of(context).textTheme.labelMedium
?.copyWith(fontWeight: FontWeight.w600),
),
],
),
),
],
),
),
),
),
),
const SizedBox(height: 16),
Text('Determinate Progress Control (${_sliderVal.toInt()}%):'),
Slider(
value: _sliderVal,
max: 100,
divisions: 100,
label: '${_sliderVal.toInt()}%',
onChanged: (v) => setState(() => _sliderVal = v),
),
Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.center,
children: [0.0, 25.0, 50.0, 75.0, 95.0, 100.0].map((preset) {
return OutlinedButton(
onPressed: () => setState(() => _sliderVal = preset),
style: OutlinedButton.styleFrom(
visualDensity: VisualDensity.compact,
),
child: Text('${preset.toInt()}%'),
);
}).toList(),
),
const SizedBox(height: 28),
_buildSectionHeader(
'Motion Physics & Curves (ExpressiveMotion & ExpressivePressable)',
'Official animation tokens with spring bounce physics (Spring Physics).',
),
const SizedBox(height: 12),
_buildMotionCard(
title: 'ExpressiveMotion.springBouncy',
subtitle:
'Cubic(0.34, 1.35, 0.64, 1) — Smooth and energetic overshoot.',
scale: _scaleBouncy,
curve: ExpressiveMotion.springBouncy,
color: const Color(0xFF1E88E5),
onTap: () async {
setState(() => _scaleBouncy = 0.85);
await Future.delayed(ExpressiveMotion.durationShort);
if (mounted) setState(() => _scaleBouncy = 1.0);
},
),
const SizedBox(height: 12),
_buildMotionCard(
title: 'ExpressiveMotion.springGentle',
subtitle: 'Cubic(0.2, 0, 0, 1) — Smooth and progressive damping.',
scale: _scaleGentle,
curve: ExpressiveMotion.springGentle,
color: const Color(0xFF7B1FA2),
onTap: () async {
setState(() => _scaleGentle = 0.85);
await Future.delayed(ExpressiveMotion.durationShort);
if (mounted) setState(() => _scaleGentle = 1.0);
},
),
const SizedBox(height: 12),
_buildMotionCard(
title: 'ExpressiveMotion.emphasized',
subtitle: 'Curves.easeInOutCubicEmphasized — State transition and morphing.',
scale: _scaleSnappy,
curve: ExpressiveMotion.emphasized,
color: const Color(0xFF00897B),
onTap: () async {
setState(() => _scaleSnappy = 0.85);
await Future.delayed(ExpressiveMotion.durationShort);
if (mounted) setState(() => _scaleSnappy = 1.0);
},
),
const SizedBox(height: 16),
ExpressivePressable(
onTap: () => _showFeedback(
'ExpressivePressable pressed with tactile feedback',
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(16),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
HugeIcon(
icon: HugeIcons.strokeRoundedCursorPointer01,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
const SizedBox(width: 8),
Text(
'ExpressivePressable (Spring press effect)',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
],
),
),
),
const SizedBox(height: 80),
],
);
}
// ---------------------------------------------------------------------------
// TAB 4: NAVIGATION AND FLOATING ACTION BUTTONS (FAB)
// ---------------------------------------------------------------------------
Widget _buildNavigationAndFabTab() {
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
_buildSectionHeader(
'Floating Action Buttons (ExpressiveFab & ExpressiveExtendedFab)',
'Sizes: small (44dp), regular (56dp), large (80dp), and extended.',
),
const SizedBox(height: 16),
Wrap(
spacing: 16,
runSpacing: 16,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
ExpressiveFab(
icon: const HugeIcon(icon: HugeIcons.strokeRoundedAdd01),
size: ExpressiveFabSize.small,
tooltip: 'Small FAB (44dp)',
onPressed: () => _showFeedback('Small FAB pressed'),
),
ExpressiveFab(
icon: const HugeIcon(icon: HugeIcons.strokeRoundedEdit02),
size: ExpressiveFabSize.regular,
tooltip: 'Regular FAB (56dp)',
onPressed: () => _showFeedback('Regular FAB pressed'),
),
ExpressiveFab(
icon: const HugeIcon(icon: HugeIcons.strokeRoundedPaintBrush01),
size: ExpressiveFabSize.large,
tooltip: 'Large FAB (80dp)',
onPressed: () => _showFeedback('Large FAB pressed'),
),
],
),
const SizedBox(height: 16),
ExpressiveExtendedFab(
icon: const HugeIcon(icon: HugeIcons.strokeRoundedTaskAdd01),
label: 'New Expressive Task',
onPressed: () => _showFeedback('Extended FAB pressed'),
),
const SizedBox(height: 24),
_buildSectionHeader(
'Expandable Floating Menu (ExpressiveFabMenu)',
'Quick action speed dial menu animated with spring bounce physics.',
),
const SizedBox(height: 8),
const ExpressiveCard(
margin: EdgeInsets.zero,
child: Row(
children: [
HugeIcon(icon: HugeIcons.strokeRoundedCursorPointer01, size: 28),
SizedBox(width: 12),
Expanded(
child: Text(
'The "+" floating button located at the bottom-right corner expands an interactive ExpressiveFabMenu with quick action options.',
style: TextStyle(fontSize: 14),
),
),
],
),
),
const SizedBox(height: 24),
_buildSectionHeader(
'Bottom Navigation Bar (ExpressiveNavigationBar)',
'Pill indicator animated with spring bounce, badges, and tonal elevation.',
),
const SizedBox(height: 8),
const ExpressiveCard(
margin: EdgeInsets.zero,
child: Row(
children: [
HugeIcon(icon: HugeIcons.strokeRoundedLayers01, size: 28),
SizedBox(width: 12),
Expanded(
child: Text(
'The active bottom navigation bar is an ExpressiveNavigationBar instance controlling the 4 tabs of this gallery.',
style: TextStyle(fontSize: 14),
),
),
],
),
),
const SizedBox(height: 24),
_buildSectionHeader(
'Expressive Top App Bar (ExpressiveTopAppBar)',
'Flat surface without unnecessary elevation, action support, and adaptive navigation back button.',
),
const SizedBox(height: 8),
ExpressiveCard(
margin: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Standard Variant vs Large Variant',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
),
const SizedBox(height: 6),
const Text(
'Allows configuring large titles, custom leading widgets, and automatic navigation buttons.',
style: TextStyle(fontSize: 13),
),
const SizedBox(height: 12),
ExpressiveButton(
label: 'View Large TopAppBar Demo',
variant: ExpressiveButtonVariant.tonal,
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: ExpressiveTopAppBar.large(
titleText: 'Large TopAppBar',
actions: [
ExpressiveIconButton.circle(
icon: const HugeIcon(
icon: HugeIcons.strokeRoundedInformationCircle,
),
onPressed: () => _showFeedback('Info modal'),
),
],
),
body: const Center(
child: Text('Secondary screen with TopAppBar.large'),
),
),
),
);
},
),
],
),
),
const SizedBox(height: 80),
],
);
}
// ---------------------------------------------------------------------------
// TAB 5: TYPOGRAPHY AND VARIABLE FONTS
// ---------------------------------------------------------------------------
Widget _buildTypographyTab() {
final theme = Theme.of(context);
const availableFonts = ExpressiveFonts.availableFonts;
const fontDescriptions = <String, String>{
'Google Sans Flex': "Modern geometric UI font with adaptive optical weights. Google's signature typeface.",
'Merriweather': 'Designed to be a pleasant, highly readable serif on screens with mild contrast.',
'Montserrat': 'Urban geometric sans-serif inspired by posters and signage of Buenos Aires.',
'Nunito': 'Well-balanced rounded terminal sans-serif, warm, approachable, and playful.',
'Open Sans': 'Neutral yet friendly humanist sans-serif optimized for mobile and web interfaces.',
'Orbitron':
'Geometric sci-fi display typeface with futuristic angular curves.',
'Playfair Display':
'High-contrast transitional serif with classical editorial elegance.',
'Roboto': "Google's versatile neo-grotesque sans-serif with natural reading rhythm and clear shapes.",
'Source Code Pro': 'Monospaced font family designed specifically for code editing and UI terminals.',
};
const fontCategories = <String, String>{
'Google Sans Flex': 'Sans-Serif • Default',
'Merriweather': 'Serif • Editorial',
'Montserrat': 'Geometric • Punchy',
'Nunito': 'Rounded • Friendly',
'Open Sans': 'Humanist • Neutral',
'Orbitron': 'Display • Sci-Fi',
'Playfair Display': 'Serif • Classical',
'Roboto': 'Neo-Grotesque • Workhorse',
'Source Code Pro': 'Monospace • Code',
};
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
_buildSectionHeader(
'Typography & Variable Fonts',
'9 curated variable fonts bundled with zero network latency. Tap any font to apply it to the entire application.',
),
const SizedBox(height: 16),
// Active Font Status & Quick Switcher
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer.withValues(alpha: 0.35),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: theme.colorScheme.primary.withValues(alpha: 0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: theme.colorScheme.primary,
shape: BoxShape.circle,
),
child: const HugeIcon(
icon: HugeIcons.strokeRoundedTextFont,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Active Application Font',
style: theme.textTheme.labelMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
Text(
availableFonts.entries
.firstWhere(
(e) => e.value == widget.selectedFont,
orElse: () => availableFonts.entries.first,
)
.key,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
],
),
),
],
),
const SizedBox(height: 14),
Text(
'Quick switcher:',
style: theme.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 6,
runSpacing: 6,
children: availableFonts.entries.map((entry) {
final isSelected = widget.selectedFont == entry.value;
return ChoiceChip(
label: Text(entry.key),
selected: isSelected,
onSelected: (_) {
widget.onFontChanged(entry.value);
_showFeedback('Theme font set to ${entry.key}');
},
);
}).toList(),
),
],
),
),
const SizedBox(height: 20),
// Interactive Custom Text Input
_buildSectionHeader(
'Live Preview Text',
'Type custom text below to preview it dynamically across all 9 fonts.',
),
const SizedBox(height: 10),
TextField(
controller: _customTextController,
decoration: InputDecoration(
hintText: 'Type sample text...',
filled: true,
fillColor: theme.colorScheme.surfaceContainerHighest.withValues(
alpha: 0.5,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
prefixIconConstraints: const BoxConstraints(
minWidth: 40,
minHeight: 40,
),
prefixIcon: const SizedBox(
width: 40,
height: 40,
child: Center(
child: HugeIcon(icon: HugeIcons.strokeRoundedEdit02, size: 20),
),
),
suffixIcon: IconButton(
icon: const HugeIcon(
icon: HugeIcons.strokeRoundedCancel01,
size: 18,
),
onPressed: () {
setState(() {
_customTextController.text =
'The quick brown fox jumps over the lazy dog.';
});
},
),
),
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 24),
// Specimen cards for each font
_buildSectionHeader(
'Bundled Font Specimens',
'Inspect letterforms, optical weights, and glyph details for each variable font family.',
),
const SizedBox(height: 12),
...availableFonts.entries.map((entry) {
final fontName = entry.key;
final fontFamily = entry.value;
final isSelected = widget.selectedFont == fontFamily;
final category = fontCategories[fontName] ?? 'Variable Font';
final description = fontDescriptions[fontName] ?? '';
final previewText = _customTextController.text.trim().isEmpty
? 'The quick brown fox jumps over the lazy dog.'
: _customTextController.text;
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: isSelected
? theme.colorScheme.primaryContainer.withValues(alpha: 0.25)
: theme.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: isSelected
? theme.colorScheme.primary
: theme.colorScheme.outlineVariant.withValues(alpha: 0.4),
width: isSelected ? 2 : 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header with font name, category, and active badge
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
fontName,
style: TextStyle(
fontFamily: fontFamily,
fontSize: 20,
fontWeight: FontWeight.bold,
color: isSelected
? theme.colorScheme.primary
: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 2),
Text(
category,
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
if (isSelected)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: theme.colorScheme.primary,
borderRadius: BorderRadius.circular(20),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
HugeIcon(
icon: HugeIcons.strokeRoundedCheckmarkBadge01,
color: Colors.white,
size: 14,
),
SizedBox(width: 4),
Text(
'Active',
style: TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 8),
Text(
description,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const Divider(height: 24),
// Live Preview in this font
Text(
previewText,
style: TextStyle(
fontFamily: fontFamily,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
// Character spectrum
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest.withValues(
alpha: 0.4,
),
borderRadius: BorderRadius.circular(12),
),
child: Text(
r'Aa Bb Cc Gg Jj Qq Ww Zz 0123456789 &€$@#%',
style: TextStyle(
fontFamily: fontFamily,
fontSize: 13,
letterSpacing: 0.5,
color: theme.colorScheme.onSurfaceVariant,
),
),
),
const SizedBox(height: 16),
// Weights Showcase
Text(
'Weight variations:',
style: theme.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
Wrap(
spacing: 12,
runSpacing: 6,
children: [
Text(
'Light (300)',
style: TextStyle(
fontFamily: fontFamily,
fontWeight: FontWeight.w300,
fontSize: 13,
),
),
Text(
'Regular (400)',
style: TextStyle(
fontFamily: fontFamily,
fontWeight: FontWeight.w400,
fontSize: 13,
),
),
Text(
'Medium (500)',
style: TextStyle(
fontFamily: fontFamily,
fontWeight: FontWeight.w500,
fontSize: 13,
),
),
Text(
'Bold (700)',
style: TextStyle(
fontFamily: fontFamily,
fontWeight: FontWeight.w700,
fontSize: 13,
),
),
Text(
'Italic',
style: TextStyle(
fontFamily: fontFamily,
fontStyle: FontStyle.italic,
fontSize: 13,
),
),
],
),
const SizedBox(height: 16),
// Action to set as app font
Align(
alignment: Alignment.centerRight,
child: ExpressiveButton(
label: isSelected ? 'Active App Font' : 'Apply to Gallery',
icon: HugeIcon(
icon: isSelected
? HugeIcons.strokeRoundedCheckmarkBadge01
: HugeIcons.strokeRoundedCheckmarkCircle02,
size: 16,
),
variant: isSelected
? ExpressiveButtonVariant.filled
: ExpressiveButtonVariant.tonal,
onPressed: () {
widget.onFontChanged(fontFamily);
_showFeedback('$fontName applied to the gallery theme');
},
),
),
],
),
);
}),
const SizedBox(height: 16),
// Type Hierarchy in Current Theme
_buildSectionHeader(
'Active Type Scale Hierarchy',
'Standard Material 3 Expressive typography scale rendered in the active font.',
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.4),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Display Large',
style: theme.textTheme.displayLarge?.copyWith(fontSize: 32),
),
const Divider(height: 20),
Text('Headline Medium', style: theme.textTheme.headlineMedium),
const Divider(height: 20),
Text('Title Large', style: theme.textTheme.titleLarge),
const Divider(height: 20),
Text('Body Large', style: theme.textTheme.bodyLarge),
const Divider(height: 20),
Text('Label Medium', style: theme.textTheme.labelMedium),
],
),
),
const SizedBox(height: 80),
],
);
}
// ---------------------------------------------------------------------------
// HELPERS
// ---------------------------------------------------------------------------
Widget _buildSectionHeader(String title, String subtitle) {
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
);
}
Widget _buildMotionCard({
required String title,
required String subtitle,
required double scale,
required Curve curve,
required Color color,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: AnimatedScale(
scale: scale,
duration: ExpressiveMotion.durationShort,
curve: curve,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color.withValues(alpha: 0.3)),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
child: const HugeIcon(
icon: HugeIcons.strokeRoundedCursorPointer01,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: color,
),
),
const SizedBox(height: 2),
Text(subtitle, style: const TextStyle(fontSize: 12)),
],
),
),
const Text(
'Tap!',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
),
],
),
),
),
);
}
}