Refactor button component and add new sections for loading indicators, icons, and navigation; update enums and pubspec description
This commit is contained in:
parent
62ecb86b76
commit
020db0ac38
23 changed files with 1033 additions and 828 deletions
|
|
@ -1,5 +1,15 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_collection/m3e_collection.dart';
|
||||
import 'package:m3e_gallery/sections/button_section.dart';
|
||||
import 'package:m3e_gallery/sections/fab_section.dart';
|
||||
import 'package:m3e_gallery/sections/icon_button_section.dart';
|
||||
import 'package:m3e_gallery/sections/loading_indicator_section.dart';
|
||||
import 'package:m3e_gallery/sections/navigation_section.dart';
|
||||
import 'package:m3e_gallery/sections/progress_section.dart';
|
||||
import 'package:m3e_gallery/sections/section_card.dart';
|
||||
import 'package:m3e_gallery/sections/slider_section.dart';
|
||||
import 'package:m3e_gallery/sections/split_button_section.dart';
|
||||
import 'package:m3e_gallery/sections/toolbar_section.dart';
|
||||
|
||||
void main() => runApp(const GalleryApp());
|
||||
|
||||
|
|
@ -26,19 +36,8 @@ class GalleryHome extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _GalleryHomeState extends State<GalleryHome> {
|
||||
// Navigation examples
|
||||
int _navBarIndex = 0;
|
||||
int _railIndex = 0;
|
||||
|
||||
// Slider examples
|
||||
double _sliderValue = 0.4;
|
||||
RangeValues _rangeValues = const RangeValues(0.25, 0.75);
|
||||
|
||||
// Progress examples
|
||||
double _progressValue = 0.6;
|
||||
|
||||
void onPressed() {
|
||||
// Placeholder function for button onPressed
|
||||
// Placeholder function onPressed
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -55,478 +54,7 @@ class _GalleryHomeState extends State<GalleryHome> {
|
|||
Icon(Icons.more_vert),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// Icon buttons
|
||||
Text('IconButtonM3E', style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
IconButtonM3E(
|
||||
icon: Icon(Icons.favorite),
|
||||
variant: IconButtonM3EVariant.filled,
|
||||
onPressed: onPressed),
|
||||
IconButtonM3E(
|
||||
icon: Icon(Icons.favorite),
|
||||
variant: IconButtonM3EVariant.tonal,
|
||||
onPressed: onPressed),
|
||||
IconButtonM3E(
|
||||
icon: Icon(Icons.favorite),
|
||||
variant: IconButtonM3EVariant.outlined,
|
||||
onPressed: onPressed),
|
||||
IconButtonM3E(
|
||||
icon: Icon(Icons.favorite),
|
||||
variant: IconButtonM3EVariant.standard,
|
||||
onPressed: onPressed),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
// Split Button
|
||||
Text('SplitButtonM3E', style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
SplitButtonM3E<String>(
|
||||
label: 'Primary',
|
||||
onPressed: () {},
|
||||
items: const [
|
||||
SplitButtonM3EItem<String>(value: 'one', child: 'One'),
|
||||
SplitButtonM3EItem<String>(value: 'two', child: 'Two'),
|
||||
],
|
||||
),
|
||||
SplitButtonM3E<String>(
|
||||
label: 'Tonal',
|
||||
emphasis: SplitButtonM3EEmphasis.tonal,
|
||||
onPressed: () {},
|
||||
items: const [
|
||||
SplitButtonM3EItem<String>(value: 'one', child: 'One'),
|
||||
SplitButtonM3EItem<String>(value: 'two', child: 'Two'),
|
||||
],
|
||||
),
|
||||
SplitButtonM3E<String>(
|
||||
label: 'Outlined',
|
||||
emphasis: SplitButtonM3EEmphasis.outlined,
|
||||
onPressed: () {},
|
||||
items: const [
|
||||
SplitButtonM3EItem<String>(value: 'one', child: 'One'),
|
||||
SplitButtonM3EItem<String>(value: 'two', child: 'Two'),
|
||||
],
|
||||
),
|
||||
SplitButtonM3E<String>(
|
||||
label: 'Text',
|
||||
emphasis: SplitButtonM3EEmphasis.text,
|
||||
onPressed: () {},
|
||||
items: const [
|
||||
SplitButtonM3EItem<String>(value: 'one', child: 'One'),
|
||||
SplitButtonM3EItem<String>(value: 'two', child: 'Two'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
// Buttons
|
||||
Text('ButtonM3E', style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(spacing: 12, runSpacing: 12, children: [
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
ButtonM3E(
|
||||
labelText: 'Filled',
|
||||
variant: ButtonM3EVariant.filled,
|
||||
onPressed: onPressed),
|
||||
ButtonM3E(
|
||||
labelText: 'Tonal',
|
||||
variant: ButtonM3EVariant.tonal,
|
||||
onPressed: onPressed),
|
||||
ButtonM3E(
|
||||
labelText: 'Outlined',
|
||||
variant: ButtonM3EVariant.outlined,
|
||||
onPressed: onPressed),
|
||||
ButtonM3E(
|
||||
labelText: 'Text',
|
||||
variant: ButtonM3EVariant.text,
|
||||
onPressed: onPressed),
|
||||
ButtonM3E(
|
||||
labelText: 'Elevated',
|
||||
variant: ButtonM3EVariant.elevated,
|
||||
onPressed: onPressed),
|
||||
],
|
||||
),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
ButtonM3E(
|
||||
labelText: 'Filled',
|
||||
variant: ButtonM3EVariant.filled,
|
||||
shapeFamily: ButtonM3EShapeFamily.square,
|
||||
onPressed: onPressed),
|
||||
ButtonM3E(
|
||||
labelText: 'Tonal',
|
||||
variant: ButtonM3EVariant.tonal,
|
||||
shapeFamily: ButtonM3EShapeFamily.square,
|
||||
onPressed: onPressed),
|
||||
ButtonM3E(
|
||||
labelText: 'Outlined',
|
||||
variant: ButtonM3EVariant.outlined,
|
||||
shapeFamily: ButtonM3EShapeFamily.square,
|
||||
onPressed: onPressed),
|
||||
ButtonM3E(
|
||||
labelText: 'Text',
|
||||
variant: ButtonM3EVariant.text,
|
||||
shapeFamily: ButtonM3EShapeFamily.square,
|
||||
onPressed: onPressed),
|
||||
ButtonM3E(
|
||||
labelText: 'Elevated',
|
||||
variant: ButtonM3EVariant.elevated,
|
||||
shapeFamily: ButtonM3EShapeFamily.square,
|
||||
onPressed: onPressed),
|
||||
],
|
||||
),
|
||||
]),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
// Button groups
|
||||
Text('ButtonGroupM3E', style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
// Standard group, round, md
|
||||
ButtonGroupM3E(
|
||||
type: ButtonGroupM3EType.standard,
|
||||
shape: ButtonGroupM3EShape.round,
|
||||
size: ButtonGroupM3ESize.md,
|
||||
children: const [
|
||||
IconButtonM3E(icon: Icon(Icons.skip_previous)),
|
||||
IconButtonM3E(icon: Icon(Icons.play_arrow)),
|
||||
IconButtonM3E(icon: Icon(Icons.skip_next)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Connected group with divider and clipping for outer corners
|
||||
ButtonGroupM3E(
|
||||
type: ButtonGroupM3EType.connected,
|
||||
shape: ButtonGroupM3EShape.round,
|
||||
size: ButtonGroupM3ESize.lg,
|
||||
showDividers: true,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
equalizeWidths: true,
|
||||
semanticLabel: 'Actions',
|
||||
children: [
|
||||
SplitButtonM3E<String>(
|
||||
label: 'Primary',
|
||||
onPressed: () {},
|
||||
items: const [
|
||||
SplitButtonM3EItem<String>(value: 'one', child: 'One'),
|
||||
SplitButtonM3EItem<String>(value: 'two', child: 'Two'),
|
||||
],
|
||||
),
|
||||
SplitButtonM3E<String>(
|
||||
label: 'Tonal',
|
||||
emphasis: SplitButtonM3EEmphasis.tonal,
|
||||
onPressed: () {},
|
||||
items: const [
|
||||
SplitButtonM3EItem<String>(value: 'one', child: 'One'),
|
||||
SplitButtonM3EItem<String>(value: 'two', child: 'Two'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Square, compact, vertical wrap
|
||||
ButtonGroupM3E(
|
||||
type: ButtonGroupM3EType.standard,
|
||||
shape: ButtonGroupM3EShape.square,
|
||||
size: ButtonGroupM3ESize.sm,
|
||||
density: ButtonGroupM3EDensity.compact,
|
||||
direction: Axis.vertical,
|
||||
children: const [
|
||||
IconButtonM3E(icon: Icon(Icons.view_agenda_outlined)),
|
||||
IconButtonM3E(icon: Icon(Icons.table_rows_outlined)),
|
||||
IconButtonM3E(icon: Icon(Icons.grid_view_outlined)),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
// Toolbar
|
||||
Text('ToolbarM3E', style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
ToolbarM3E(
|
||||
titleText: 'Toolbar Title',
|
||||
subtitleText: 'Subtitle',
|
||||
actions: [
|
||||
ToolbarActionM3E(icon: Icons.search, onPressed: () {}),
|
||||
ToolbarActionM3E(icon: Icons.share, onPressed: () {}),
|
||||
ToolbarActionM3E(
|
||||
icon: Icons.delete,
|
||||
onPressed: () {},
|
||||
isDestructive: true,
|
||||
label: 'Delete'),
|
||||
ToolbarActionM3E(
|
||||
icon: Icons.settings, onPressed: () {}, label: 'Settings'),
|
||||
],
|
||||
maxInlineActions: 2,
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
// FABs
|
||||
Text('FabM3E', style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: const [
|
||||
FabM3E(icon: Icon(Icons.add)),
|
||||
FabM3E(icon: Icon(Icons.edit), kind: FabM3EKind.secondary),
|
||||
FabM3E(icon: Icon(Icons.share), kind: FabM3EKind.tertiary),
|
||||
FabM3E(icon: Icon(Icons.more_horiz), kind: FabM3EKind.surface),
|
||||
FabM3E(icon: Icon(Icons.add), size: FabM3ESize.small),
|
||||
FabM3E(icon: Icon(Icons.add), size: FabM3ESize.large),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: const [
|
||||
ExtendedFabM3E(label: Text('Create'), icon: Icon(Icons.add)),
|
||||
ExtendedFabM3E(
|
||||
label: Text('Edit'),
|
||||
icon: Icon(Icons.edit),
|
||||
kind: FabM3EKind.secondary),
|
||||
ExtendedFabM3E(
|
||||
label: Text('Share'),
|
||||
icon: Icon(Icons.share),
|
||||
kind: FabM3EKind.tertiary),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: m3e.shapes.round.lg,
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: FabMenuM3E(
|
||||
primaryFab: const FabM3E(icon: Icon(Icons.menu)),
|
||||
items: [
|
||||
FabMenuItem(
|
||||
icon: const Icon(Icons.image),
|
||||
label: const Text('Image'),
|
||||
onPressed: () {}),
|
||||
FabMenuItem(
|
||||
icon: const Icon(Icons.camera_alt),
|
||||
label: const Text('Camera'),
|
||||
onPressed: () {}),
|
||||
FabMenuItem(
|
||||
icon: const Icon(Icons.file_upload),
|
||||
label: const Text('Upload'),
|
||||
onPressed: () {}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
// Loading indicators
|
||||
Text('LoadingIndicatorM3E',
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: const [
|
||||
LoadingIndicatorM3E(
|
||||
variant: LoadingIndicatorM3EVariant.defaultStyle),
|
||||
LoadingIndicatorM3E(
|
||||
variant: LoadingIndicatorM3EVariant.contained),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
// Progress indicators
|
||||
Text('ProgressIndicatorM3E',
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
const CircularProgressM3E(),
|
||||
const CircularProgressM3E(size: ProgressM3ESize.small),
|
||||
CircularProgressM3E(
|
||||
size: ProgressM3ESize.large,
|
||||
value: _progressValue,
|
||||
showCenterLabel: true),
|
||||
const LinearProgressM3E(minWidth: 200),
|
||||
LinearProgressM3E(minWidth: 200, value: _progressValue),
|
||||
const LinearProgressM3E(
|
||||
minWidth: 200,
|
||||
variant: LinearProgressM3EVariant.indeterminate),
|
||||
const LinearProgressM3E(
|
||||
minWidth: 200, variant: LinearProgressM3EVariant.query),
|
||||
const LinearProgressM3E(
|
||||
minWidth: 200,
|
||||
variant: LinearProgressM3EVariant.buffer,
|
||||
bufferValue: 0.8,
|
||||
value: 0.4),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
// Sliders
|
||||
Text('SliderM3E', style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
SliderM3E(
|
||||
value: _sliderValue,
|
||||
onChanged: (v) => setState(() => _sliderValue = v),
|
||||
min: 0,
|
||||
max: 100,
|
||||
label: '${_sliderValue.toStringAsFixed(0)}',
|
||||
startIcon: const Icon(Icons.volume_mute),
|
||||
endIcon: const Icon(Icons.volume_up),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
RangeSliderM3E(
|
||||
values: _rangeValues,
|
||||
onChanged: (v) => setState(() => _rangeValues = v),
|
||||
min: 0,
|
||||
max: 100,
|
||||
labels: RangeLabels(
|
||||
_rangeValues.start.toStringAsFixed(0),
|
||||
_rangeValues.end.toStringAsFixed(0),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
// Navigation Bar
|
||||
Text('NavigationBarM3E',
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
NavigationBarM3E(
|
||||
selectedIndex: _navBarIndex,
|
||||
onDestinationSelected: (i) => setState(() => _navBarIndex = i),
|
||||
indicatorStyle: NavBarM3EIndicatorStyle.pill,
|
||||
destinations: const [
|
||||
NavigationDestinationM3E(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
selectedIcon: Icon(Icons.home),
|
||||
label: 'Home'),
|
||||
NavigationDestinationM3E(
|
||||
icon: Icon(Icons.search_outlined),
|
||||
selectedIcon: Icon(Icons.search),
|
||||
label: 'Search',
|
||||
badgeDot: true),
|
||||
NavigationDestinationM3E(
|
||||
icon: Icon(Icons.favorite_outline),
|
||||
selectedIcon: Icon(Icons.favorite),
|
||||
label: 'Favorites',
|
||||
badgeCount: 3),
|
||||
NavigationDestinationM3E(
|
||||
icon: Icon(Icons.person_outline),
|
||||
selectedIcon: Icon(Icons.person),
|
||||
label: 'Profile'),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
// Navigation Rail
|
||||
Text('NavigationRailM3E',
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: m3e.shapes.round.lg,
|
||||
),
|
||||
height: 180,
|
||||
child: Row(
|
||||
children: [
|
||||
NavigationRailM3E(
|
||||
selectedIndex: _railIndex,
|
||||
onDestinationSelected: (i) => setState(() => _railIndex = i),
|
||||
indicatorStyle: RailIndicatorStyle.pill,
|
||||
destinations: const [
|
||||
RailDestinationM3E(
|
||||
icon: Icon(Icons.dashboard_outlined),
|
||||
selectedIcon: Icon(Icons.dashboard),
|
||||
label: 'Dashboard'),
|
||||
RailDestinationM3E(
|
||||
icon: Icon(Icons.analytics_outlined),
|
||||
selectedIcon: Icon(Icons.analytics),
|
||||
label: 'Reports'),
|
||||
RailDestinationM3E(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
selectedIcon: Icon(Icons.settings),
|
||||
label: 'Settings'),
|
||||
],
|
||||
),
|
||||
const VerticalDivider(width: 1),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text('Selected: $_railIndex'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
// Sliver App Bar demo route
|
||||
Text('App bars', style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
children: [
|
||||
const ButtonM3E(labelText: 'Open SliverAppBarM3E Demo'),
|
||||
// Use GestureDetector to navigate when tapping the button label area
|
||||
]
|
||||
.map((w) => GestureDetector(
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const _SliverAppBarDemoPage()),
|
||||
),
|
||||
child: w,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 48),
|
||||
// Theming surface example remains
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: m3e.colors.surfaceStrong,
|
||||
borderRadius: m3e.shapes.square.lg,
|
||||
),
|
||||
child: Text('Surface strong example',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(color: cs.onSurface)),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: const SectionedGallery(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -558,3 +86,38 @@ class _SliverAppBarDemoPage extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SectionedGallery extends StatelessWidget {
|
||||
const SectionedGallery({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
const IconButtonSection(),
|
||||
const SplitButtonSection(),
|
||||
const ButtonSection(),
|
||||
const ToolbarSection(),
|
||||
const FabSection(),
|
||||
const LoadingIndicatorSection(),
|
||||
const ProgressSection(),
|
||||
const SliderSection(),
|
||||
const NavigationSection(),
|
||||
SectionCard(
|
||||
title: 'App bars',
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ButtonM3E(
|
||||
label: Text('Open SliverAppBarM3E Demo'),
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const _SliverAppBarDemoPage()),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
50
apps/gallery/lib/sections/button_section.dart
Normal file
50
apps/gallery/lib/sections/button_section.dart
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_collection/m3e_collection.dart';
|
||||
|
||||
import 'section_card.dart';
|
||||
|
||||
class ButtonSection extends StatelessWidget {
|
||||
const ButtonSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SectionCard(
|
||||
title: 'ButtonM3E',
|
||||
subtitle:
|
||||
'Generated from enums: variant × size; grouped by shape family.',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final shape in ButtonM3EShape.values) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text('${shape.name} shape',
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
for (final size in ButtonM3ESize.values) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 4),
|
||||
child: Text('size: ${size.name}',
|
||||
style: Theme.of(context).textTheme.labelLarge),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
for (final variant in ButtonM3EStyle.values)
|
||||
ButtonM3E(
|
||||
label: Text(variant.name),
|
||||
style: variant,
|
||||
size: size,
|
||||
shape: shape,
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
60
apps/gallery/lib/sections/fab_section.dart
Normal file
60
apps/gallery/lib/sections/fab_section.dart
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_collection/m3e_collection.dart';
|
||||
|
||||
import 'section_card.dart';
|
||||
|
||||
class FabSection extends StatelessWidget {
|
||||
const FabSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
void onPressed() {
|
||||
// Placeholder function onPressed
|
||||
}
|
||||
|
||||
return SectionCard(
|
||||
title: 'FabM3E',
|
||||
subtitle:
|
||||
'Generated from enums: kind × size. Includes Extended FAB examples.',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
for (final kind in FabM3EKind.values)
|
||||
for (final size in FabM3ESize.values)
|
||||
FabM3E(
|
||||
icon: const Icon(Icons.add),
|
||||
kind: kind,
|
||||
size: size,
|
||||
onPressed: onPressed),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
ExtendedFabM3E(
|
||||
label: Text('Create'),
|
||||
icon: Icon(Icons.add),
|
||||
onPressed: onPressed),
|
||||
ExtendedFabM3E(
|
||||
label: Text('Edit'),
|
||||
icon: Icon(Icons.edit),
|
||||
kind: FabM3EKind.secondary,
|
||||
onPressed: onPressed),
|
||||
ExtendedFabM3E(
|
||||
label: Text('Share'),
|
||||
icon: Icon(Icons.share),
|
||||
kind: FabM3EKind.tertiary,
|
||||
onPressed: onPressed),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
40
apps/gallery/lib/sections/icon_button_section.dart
Normal file
40
apps/gallery/lib/sections/icon_button_section.dart
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_collection/m3e_collection.dart';
|
||||
|
||||
import 'section_card.dart';
|
||||
|
||||
class IconButtonSection extends StatelessWidget {
|
||||
const IconButtonSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SectionCard(
|
||||
title: 'IconButtonM3E',
|
||||
subtitle: 'Generated from enums: variant × size (round shape, default width).',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final variant in IconButtonM3EVariant.values) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(variant.name, style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
for (final size in IconButtonM3ESize.values)
|
||||
IconButtonM3E(
|
||||
icon: const Icon(Icons.favorite),
|
||||
variant: variant,
|
||||
size: size,
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
25
apps/gallery/lib/sections/loading_indicator_section.dart
Normal file
25
apps/gallery/lib/sections/loading_indicator_section.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_collection/m3e_collection.dart';
|
||||
|
||||
import 'section_card.dart';
|
||||
|
||||
class LoadingIndicatorSection extends StatelessWidget {
|
||||
const LoadingIndicatorSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SectionCard(
|
||||
title: 'LoadingIndicatorM3E',
|
||||
subtitle: 'Generated from enums: variant values.',
|
||||
child: Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
for (final v in LoadingIndicatorM3EVariant.values)
|
||||
LoadingIndicatorM3E(variant: v),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
95
apps/gallery/lib/sections/navigation_section.dart
Normal file
95
apps/gallery/lib/sections/navigation_section.dart
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_collection/m3e_collection.dart';
|
||||
|
||||
import 'section_card.dart';
|
||||
|
||||
class NavigationSection extends StatefulWidget {
|
||||
const NavigationSection({super.key});
|
||||
|
||||
@override
|
||||
State<NavigationSection> createState() => _NavigationSectionState();
|
||||
}
|
||||
|
||||
class _NavigationSectionState extends State<NavigationSection> {
|
||||
int _barIndex = 0;
|
||||
int _railIndex = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final m3e = theme.extension<M3ETheme>() ?? M3ETheme.defaults(theme.colorScheme);
|
||||
|
||||
return SectionCard(
|
||||
title: 'Navigation',
|
||||
subtitle: 'Generated from enums: NavBar indicator styles and Rail indicator styles.',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text('Navigation Bar', style: theme.textTheme.titleMedium),
|
||||
),
|
||||
Wrap(
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
for (final style in NavBarM3EIndicatorStyle.values)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Text('indicator: ${style.name}', style: theme.textTheme.labelLarge),
|
||||
),
|
||||
NavigationBarM3E(
|
||||
selectedIndex: _barIndex,
|
||||
onDestinationSelected: (i) => setState(() => _barIndex = i),
|
||||
indicatorStyle: style,
|
||||
destinations: const [
|
||||
NavigationDestinationM3E(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Home'),
|
||||
NavigationDestinationM3E(icon: Icon(Icons.search_outlined), selectedIcon: Icon(Icons.search), label: 'Search', badgeDot: true),
|
||||
NavigationDestinationM3E(icon: Icon(Icons.favorite_outline), selectedIcon: Icon(Icons.favorite), label: 'Favorites', badgeCount: 2),
|
||||
NavigationDestinationM3E(icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: 'Profile'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text('Navigation Rail', style: theme.textTheme.titleMedium),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: m3e.shapes.round.lg,
|
||||
),
|
||||
height: 220,
|
||||
child: Row(
|
||||
children: [
|
||||
for (final style in RailIndicatorStyle.values) ...[
|
||||
NavigationRailM3E(
|
||||
selectedIndex: _railIndex,
|
||||
onDestinationSelected: (i) => setState(() => _railIndex = i),
|
||||
indicatorStyle: style,
|
||||
destinations: const [
|
||||
RailDestinationM3E(icon: Icon(Icons.dashboard_outlined), selectedIcon: Icon(Icons.dashboard), label: 'Dash'),
|
||||
RailDestinationM3E(icon: Icon(Icons.analytics_outlined), selectedIcon: Icon(Icons.analytics), label: 'Reports'),
|
||||
RailDestinationM3E(icon: Icon(Icons.settings_outlined), selectedIcon: Icon(Icons.settings), label: 'Settings'),
|
||||
],
|
||||
),
|
||||
const VerticalDivider(width: 1),
|
||||
],
|
||||
Expanded(
|
||||
child: Center(child: Text('Selected: $_railIndex')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
107
apps/gallery/lib/sections/progress_section.dart
Normal file
107
apps/gallery/lib/sections/progress_section.dart
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_collection/m3e_collection.dart';
|
||||
|
||||
import 'section_card.dart';
|
||||
|
||||
class ProgressSection extends StatelessWidget {
|
||||
const ProgressSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SectionCard(
|
||||
title: 'ProgressIndicatorM3E',
|
||||
subtitle: 'Generated from enums: circular sizes and linear variants.',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text('Circular - Wavy',
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
for (final s in ProgressM3ESize.values) ...[
|
||||
CircularProgressM3E(
|
||||
size: s,
|
||||
value: 0.4,
|
||||
),
|
||||
CircularProgressM3E(
|
||||
size: s,
|
||||
value: 0.6,
|
||||
showCenterLabel: s != ProgressM3ESize.small),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text('Circular - Flat',
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
for (final s in ProgressM3ESize.values) ...[
|
||||
CircularProgressM3E(
|
||||
size: s,
|
||||
value: 0.4,
|
||||
shape: CircularBarShapeM3E.flat,
|
||||
),
|
||||
CircularProgressM3E(
|
||||
size: s,
|
||||
shape: CircularBarShapeM3E.flat,
|
||||
value: 0.6,
|
||||
showCenterLabel: s != ProgressM3ESize.small),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text('Linear - Wavy',
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
for (final v in LinearProgressM3EVariant.values)
|
||||
LinearProgressM3E(
|
||||
minWidth: 220,
|
||||
variant: v,
|
||||
value: v == LinearProgressM3EVariant.determinate ? 0.6 : null,
|
||||
bufferValue:
|
||||
v == LinearProgressM3EVariant.buffer ? 0.8 : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text('Linear - Flat',
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
for (final v in LinearProgressM3EVariant.values)
|
||||
LinearProgressM3E(
|
||||
minWidth: 220,
|
||||
variant: v,
|
||||
shape: LinearBarShapeM3E.flat,
|
||||
value: v == LinearProgressM3EVariant.determinate ? 0.6 : null,
|
||||
bufferValue:
|
||||
v == LinearProgressM3EVariant.buffer ? 0.8 : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
40
apps/gallery/lib/sections/section_card.dart
Normal file
40
apps/gallery/lib/sections/section_card.dart
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_collection/m3e_collection.dart';
|
||||
|
||||
class SectionCard extends StatelessWidget {
|
||||
const SectionCard(
|
||||
{super.key, required this.title, this.subtitle, required this.child});
|
||||
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final m3e =
|
||||
theme.extension<M3ETheme>() ?? M3ETheme.defaults(theme.colorScheme);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(title, style: theme.textTheme.titleLarge),
|
||||
subtitle: subtitle == null ? null : Text(subtitle!),
|
||||
),
|
||||
Material(
|
||||
color: theme.colorScheme.surfaceContainerLow,
|
||||
borderRadius: m3e.shapes.round.lg,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
73
apps/gallery/lib/sections/slider_section.dart
Normal file
73
apps/gallery/lib/sections/slider_section.dart
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_collection/m3e_collection.dart';
|
||||
|
||||
import 'section_card.dart';
|
||||
|
||||
class SliderSection extends StatefulWidget {
|
||||
const SliderSection({super.key});
|
||||
|
||||
@override
|
||||
State<SliderSection> createState() => _SliderSectionState();
|
||||
}
|
||||
|
||||
class _SliderSectionState extends State<SliderSection> {
|
||||
double _value = 40;
|
||||
RangeValues _range = const RangeValues(25, 75);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SectionCard(
|
||||
title: 'SliderM3E',
|
||||
subtitle: 'Generated from enums: size × emphasis (round shape).',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final size in SliderM3ESize.values) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text('size: ${size.name}', style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
Wrap(
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
for (final emp in SliderM3EEmphasis.values)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('emphasis: ${emp.name}', style: Theme.of(context).textTheme.labelLarge),
|
||||
SliderM3E(
|
||||
value: _value,
|
||||
onChanged: (v) => setState(() => _value = v),
|
||||
min: 0,
|
||||
max: 100,
|
||||
label: _value.toStringAsFixed(0),
|
||||
size: size,
|
||||
emphasis: emp,
|
||||
startIcon: const Icon(Icons.volume_mute),
|
||||
endIcon: const Icon(Icons.volume_up),
|
||||
),
|
||||
RangeSliderM3E(
|
||||
values: _range,
|
||||
onChanged: (v) => setState(() => _range = v),
|
||||
min: 0,
|
||||
max: 100,
|
||||
size: size,
|
||||
emphasis: emp,
|
||||
labels: RangeLabels(
|
||||
_range.start.toStringAsFixed(0),
|
||||
_range.end.toStringAsFixed(0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
47
apps/gallery/lib/sections/split_button_section.dart
Normal file
47
apps/gallery/lib/sections/split_button_section.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_collection/m3e_collection.dart';
|
||||
|
||||
import 'section_card.dart';
|
||||
|
||||
class SplitButtonSection extends StatelessWidget {
|
||||
const SplitButtonSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = const [
|
||||
SplitButtonM3EItem<String>(value: 'one', child: 'One'),
|
||||
SplitButtonM3EItem<String>(value: 'two', child: 'Two'),
|
||||
SplitButtonM3EItem<String>(value: 'three', child: 'Three'),
|
||||
];
|
||||
|
||||
return SectionCard(
|
||||
title: 'SplitButtonM3E',
|
||||
subtitle: 'Generated from enums: emphasis × size (round shape).',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final emphasis in SplitButtonM3EEmphasis.values) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(emphasis.name, style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
for (final size in SplitButtonM3ESize.values)
|
||||
SplitButtonM3E<String>(
|
||||
label: emphasis.name,
|
||||
size: size,
|
||||
emphasis: emphasis,
|
||||
onPressed: () {},
|
||||
items: items,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
48
apps/gallery/lib/sections/toolbar_section.dart
Normal file
48
apps/gallery/lib/sections/toolbar_section.dart
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_collection/m3e_collection.dart';
|
||||
|
||||
import 'section_card.dart';
|
||||
|
||||
class ToolbarSection extends StatelessWidget {
|
||||
const ToolbarSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final actions = [
|
||||
ToolbarActionM3E(icon: Icons.search, onPressed: () {}),
|
||||
ToolbarActionM3E(icon: Icons.share, onPressed: () {}),
|
||||
ToolbarActionM3E(icon: Icons.delete, onPressed: () {}, isDestructive: true, label: 'Delete'),
|
||||
ToolbarActionM3E(icon: Icons.settings, onPressed: () {}, label: 'Settings'),
|
||||
];
|
||||
|
||||
return SectionCard(
|
||||
title: 'ToolbarM3E',
|
||||
subtitle: 'Generated from enums: variant × size (round shape).',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final variant in ToolbarM3EVariant.values) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(variant.name, style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
Wrap(
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
for (final size in ToolbarM3ESize.values)
|
||||
ToolbarM3E(
|
||||
titleText: 'Toolbar',
|
||||
subtitleText: 'size: ${size.name}',
|
||||
actions: actions,
|
||||
maxInlineActions: 2,
|
||||
variant: variant,
|
||||
size: size,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) ...
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
|
|||
|
|
@ -1,70 +1,7 @@
|
|||
|
||||
# button_m3e
|
||||
|
||||
Material 3 **Expressive** Buttons for Flutter, built on top of Flutter's buttons but styled via **M3E** tokens.
|
||||
Material 3 **Expressive Buttons** for Flutter — sizes XS→XL, round/square shapes,
|
||||
toggle selection, and 5 styles (filled/tonal/elevated/outlined/text).
|
||||
|
||||
Variants: **filled**, **tonal**, **outlined**, **text**, **elevated**
|
||||
Sizes: **small**, **medium**, **large**
|
||||
Shape families: **round**, **square**
|
||||
Density: **regular**, **compact**
|
||||
|
||||
> Depends on `m3e_design` (ThemeExtension with colors/typography/spacing/shapes).
|
||||
|
||||
## Monorepo Layout
|
||||
|
||||
```
|
||||
packages/
|
||||
m3e_design/
|
||||
button_m3e/
|
||||
```
|
||||
|
||||
In `pubspec.yaml` this package references `../m3e_design`.
|
||||
|
||||
## Usage
|
||||
|
||||
```dart
|
||||
import 'package:button_m3e/button_m3e.dart';
|
||||
|
||||
ButtonM3E(
|
||||
variant: ButtonM3EVariant.filled,
|
||||
size: ButtonM3ESize.medium,
|
||||
labelText: 'Continue',
|
||||
leading: const Icon(Icons.arrow_forward),
|
||||
onPressed: () {},
|
||||
);
|
||||
```
|
||||
|
||||
Full-width button:
|
||||
|
||||
```dart
|
||||
const ButtonM3E(
|
||||
variant: ButtonM3EVariant.tonal,
|
||||
size: ButtonM3ESize.large,
|
||||
labelText: 'Buy now',
|
||||
expand: true,
|
||||
);
|
||||
```
|
||||
|
||||
Outlined/Text/Elevated work similarly.
|
||||
|
||||
## Theming via `m3e_design`
|
||||
|
||||
`button_m3e` reads tokens from your theme:
|
||||
|
||||
- `m3e.colors.*` for background/foreground/border/disabled
|
||||
- `m3e.type.labelLarge` for the button label
|
||||
- `m3e.shapes.round|square` (uses `.lg` radius for buttons)
|
||||
- `m3e.spacing` for horizontal paddings (`sm`, `md`, `lg`)
|
||||
|
||||
If the extension is not present, it falls back to `M3ETheme.defaults(ColorScheme)`.
|
||||
You can still override `ThemeData.colorScheme` to influence defaults globally.
|
||||
|
||||
## Notes
|
||||
|
||||
- Label can be provided as `labelText` (String) or `label` (Widget).
|
||||
- `leading`/`trailing` are optional helpers for icons.
|
||||
- `expand: true` makes the button take full width.
|
||||
- `density: compact` slightly reduces height for each size.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
See `lib/src/button_tokens_adapter.dart` for measurements & color mapping.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
library button_m3e;
|
||||
|
||||
export 'src/enums.dart';
|
||||
export 'src/button_tokens_adapter.dart' show ButtonTokensAdapter, ButtonMeasurements;
|
||||
export 'src/button_m3e.dart';
|
||||
export 'src/button_theme_m3e.dart';
|
||||
|
|
|
|||
|
|
@ -1,144 +1,212 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'enums.dart';
|
||||
import 'button_theme_m3e.dart';
|
||||
|
||||
class ButtonM3E extends StatelessWidget {
|
||||
import 'button_tokens_adapter.dart';
|
||||
import 'enums.dart';
|
||||
|
||||
class ButtonM3E extends StatefulWidget {
|
||||
const ButtonM3E({
|
||||
super.key,
|
||||
this.onPressed,
|
||||
this.onLongPress,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.label,
|
||||
this.labelText,
|
||||
this.expand = false,
|
||||
this.variant = ButtonM3EVariant.filled,
|
||||
this.size = ButtonM3ESize.medium,
|
||||
this.shapeFamily = ButtonM3EShapeFamily.round,
|
||||
this.density = ButtonM3EDensity.regular,
|
||||
this.semanticLabel,
|
||||
}) : assert(label != null || labelText != null, 'Provide either label or labelText');
|
||||
required this.onPressed,
|
||||
required this.label,
|
||||
this.icon,
|
||||
this.style = ButtonM3EStyle.filled,
|
||||
this.size = ButtonM3ESize.sm,
|
||||
this.shape = ButtonM3EShape.round,
|
||||
this.selected = false,
|
||||
this.toggleable = false,
|
||||
this.onSelectedChange,
|
||||
this.smallPaddingDeprecated24 = false,
|
||||
this.enabled = true,
|
||||
this.statesController,
|
||||
});
|
||||
|
||||
final VoidCallback? onPressed;
|
||||
final VoidCallback? onLongPress;
|
||||
final Widget? leading;
|
||||
final Widget? trailing;
|
||||
final Widget? label;
|
||||
final String? labelText;
|
||||
final bool expand;
|
||||
|
||||
final ButtonM3EVariant variant;
|
||||
final Widget label;
|
||||
final Widget? icon;
|
||||
final ButtonM3EStyle style;
|
||||
final ButtonM3ESize size;
|
||||
final ButtonM3EShapeFamily shapeFamily;
|
||||
final ButtonM3EDensity density;
|
||||
final String? semanticLabel;
|
||||
final ButtonM3EShape shape;
|
||||
final bool selected;
|
||||
final bool toggleable;
|
||||
final ValueChanged<bool>? onSelectedChange;
|
||||
final bool smallPaddingDeprecated24;
|
||||
final bool enabled;
|
||||
final WidgetStatesController? statesController;
|
||||
|
||||
@override
|
||||
State<ButtonM3E> createState() => _ButtonM3EState();
|
||||
}
|
||||
|
||||
class _ButtonM3EState extends State<ButtonM3E> {
|
||||
late WidgetStatesController _statesController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_statesController = widget.statesController ?? WidgetStatesController();
|
||||
_syncSelectedToController();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ButtonM3E oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.selected != widget.selected && widget.toggleable) {
|
||||
_syncSelectedToController();
|
||||
}
|
||||
}
|
||||
|
||||
void _syncSelectedToController() {
|
||||
_statesController.update(WidgetState.selected, widget.selected);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = ButtonTokensAdapter(context);
|
||||
final m = t.metrics(density);
|
||||
final shape = t.shape(shapeFamily);
|
||||
final tokens = ButtonTokensAdapter(context,
|
||||
smallPaddingDeprecated24: widget.smallPaddingDeprecated24);
|
||||
final m = tokens.measurements(widget.size);
|
||||
final style = _resolveStyle(tokens, m);
|
||||
|
||||
final (minH, pad) = switch (size) {
|
||||
ButtonM3ESize.small => (m.heightSmall, m.paddingSmall),
|
||||
ButtonM3ESize.medium => (m.heightMedium, m.paddingMedium),
|
||||
ButtonM3ESize.large => (m.heightLarge, m.paddingLarge),
|
||||
};
|
||||
final onPressed = widget.enabled
|
||||
? () {
|
||||
if (widget.toggleable) {
|
||||
final newVal = !widget.selected;
|
||||
widget.onSelectedChange?.call(newVal);
|
||||
if (widget.onSelectedChange == null) {
|
||||
_statesController.update(WidgetState.selected, newVal);
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
widget.onPressed?.call();
|
||||
}
|
||||
: null;
|
||||
|
||||
final style = _styleFor(context, t, shape, minH, pad);
|
||||
final child = _buildContent(m);
|
||||
|
||||
final childLabel = label ?? Text(labelText!, overflow: TextOverflow.ellipsis);
|
||||
final content = _buildContent(context, t, childLabel);
|
||||
|
||||
final Widget btn = switch (variant) {
|
||||
ButtonM3EVariant.filled => FilledButton(style: style, onPressed: onPressed, onLongPress: onLongPress, child: content),
|
||||
ButtonM3EVariant.tonal => FilledButton.tonal(style: style, onPressed: onPressed, onLongPress: onLongPress, child: content),
|
||||
ButtonM3EVariant.outlined => OutlinedButton(style: style, onPressed: onPressed, onLongPress: onLongPress, child: content),
|
||||
ButtonM3EVariant.text => TextButton(style: style, onPressed: onPressed, onLongPress: onLongPress, child: content),
|
||||
ButtonM3EVariant.elevated => ElevatedButton(style: style, onPressed: onPressed, onLongPress: onLongPress, child: content),
|
||||
};
|
||||
|
||||
if (!expand && semanticLabel == null) return btn;
|
||||
final wrapped = expand ? SizedBox(width: double.infinity, child: btn) : btn;
|
||||
if (semanticLabel == null) return wrapped;
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: semanticLabel,
|
||||
child: wrapped,
|
||||
);
|
||||
}
|
||||
|
||||
ButtonStyle _styleFor(BuildContext context, ButtonTokensAdapter t, OutlinedBorder shape, double minH, EdgeInsetsGeometry pad) {
|
||||
switch (variant) {
|
||||
case ButtonM3EVariant.filled:
|
||||
return FilledButton.styleFrom(
|
||||
backgroundColor: t.bgFilled(),
|
||||
foregroundColor: t.fgFilled(),
|
||||
textStyle: t.labelStyle(),
|
||||
minimumSize: Size(0, minH),
|
||||
padding: pad,
|
||||
shape: shape,
|
||||
switch (widget.style) {
|
||||
case ButtonM3EStyle.filled:
|
||||
return FilledButton(
|
||||
style: style,
|
||||
onPressed: onPressed,
|
||||
statesController: _statesController,
|
||||
child: child,
|
||||
);
|
||||
case ButtonM3EVariant.tonal:
|
||||
return FilledButton.styleFrom(
|
||||
backgroundColor: t.bgTonal(),
|
||||
foregroundColor: t.fgTonal(),
|
||||
textStyle: t.labelStyle(),
|
||||
minimumSize: Size(0, minH),
|
||||
padding: pad,
|
||||
shape: shape,
|
||||
case ButtonM3EStyle.tonal:
|
||||
return FilledButton.tonal(
|
||||
style: style,
|
||||
onPressed: onPressed,
|
||||
statesController: _statesController,
|
||||
child: child,
|
||||
);
|
||||
case ButtonM3EVariant.outlined:
|
||||
return OutlinedButton.styleFrom(
|
||||
foregroundColor: t.fgOutlined(),
|
||||
textStyle: t.labelStyle(),
|
||||
minimumSize: Size(0, minH),
|
||||
padding: pad,
|
||||
shape: shape,
|
||||
side: BorderSide(color: t.borderOutlined()),
|
||||
case ButtonM3EStyle.elevated:
|
||||
return ElevatedButton(
|
||||
style: style,
|
||||
onPressed: onPressed,
|
||||
statesController: _statesController,
|
||||
child: child,
|
||||
);
|
||||
case ButtonM3EVariant.text:
|
||||
return TextButton.styleFrom(
|
||||
foregroundColor: t.fgText(),
|
||||
textStyle: t.labelStyle(),
|
||||
minimumSize: Size(0, minH),
|
||||
padding: pad,
|
||||
shape: shape,
|
||||
case ButtonM3EStyle.outlined:
|
||||
return OutlinedButton(
|
||||
style: style.copyWith(
|
||||
side: WidgetStateProperty.resolveWith((states) {
|
||||
final disabled = states.contains(WidgetState.disabled);
|
||||
return BorderSide(
|
||||
color:
|
||||
tokens.outline().withValues(alpha: disabled ? 0.12 : 1),
|
||||
width: 1);
|
||||
}),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
statesController: _statesController,
|
||||
child: child,
|
||||
);
|
||||
case ButtonM3EVariant.elevated:
|
||||
return ElevatedButton.styleFrom(
|
||||
backgroundColor: t.bgElevated(),
|
||||
foregroundColor: t.fgElevated(),
|
||||
textStyle: t.labelStyle(),
|
||||
minimumSize: Size(0, minH),
|
||||
padding: pad,
|
||||
shape: shape,
|
||||
elevation: _elevationFor(context, t),
|
||||
case ButtonM3EStyle.text:
|
||||
return TextButton(
|
||||
style: style,
|
||||
onPressed: onPressed,
|
||||
statesController: _statesController,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
double _elevationFor(BuildContext context, ButtonTokensAdapter t) {
|
||||
// Simple mapping; can be themed further via tokens.
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context, ButtonTokensAdapter t, Widget childLabel) {
|
||||
final style = t.labelStyle();
|
||||
final text = DefaultTextStyle.merge(style: style, child: childLabel);
|
||||
final hasLeading = leading != null;
|
||||
final hasTrailing = trailing != null;
|
||||
|
||||
if (!hasLeading && !hasTrailing) return text;
|
||||
|
||||
Widget _buildContent(ButtonMeasurements m) {
|
||||
final text = DefaultTextStyle.merge(child: widget.label);
|
||||
if (widget.icon == null) return text;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (hasLeading) ...[leading!, const SizedBox(width: 8)],
|
||||
Flexible(child: text),
|
||||
if (hasTrailing) ...[const SizedBox(width: 8), trailing!],
|
||||
IconTheme.merge(
|
||||
data: IconThemeData(size: m.iconSize),
|
||||
child: widget.icon!,
|
||||
),
|
||||
SizedBox(width: m.iconGap),
|
||||
text,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
OutlinedBorder _shapeFor(
|
||||
Set<WidgetState> states, ButtonTokensAdapter tokens) {
|
||||
final selected = states.contains(WidgetState.selected) || widget.selected;
|
||||
final pressed = states.contains(WidgetState.pressed);
|
||||
|
||||
OutlinedBorder round = const StadiumBorder();
|
||||
OutlinedBorder square = RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(tokens.squareRadius(widget.size)),
|
||||
);
|
||||
OutlinedBorder pressedSquare = RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(tokens.pressedRadius(widget.size)),
|
||||
);
|
||||
|
||||
OutlinedBorder base = widget.shape == ButtonM3EShape.round ? round : square;
|
||||
OutlinedBorder alt = widget.shape == ButtonM3EShape.round ? square : round;
|
||||
|
||||
if (selected && pressed) {
|
||||
return OutlinedBorder.lerp(alt, pressedSquare, 0.5)!;
|
||||
} else if (selected) {
|
||||
return alt;
|
||||
} else if (pressed) {
|
||||
return OutlinedBorder.lerp(base, pressedSquare, 0.7)!;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
ButtonStyle _resolveStyle(ButtonTokensAdapter tokens, ButtonMeasurements m) {
|
||||
final fg = WidgetStateProperty.resolveWith<Color?>((states) {
|
||||
final disabled = states.contains(WidgetState.disabled);
|
||||
final color = tokens.foreground(widget.style);
|
||||
return disabled ? color.withValues(alpha: 0.38) : color;
|
||||
});
|
||||
|
||||
final bg = WidgetStateProperty.resolveWith<Color?>((states) {
|
||||
final disabled = states.contains(WidgetState.disabled);
|
||||
final color = tokens.container(widget.style);
|
||||
if (widget.style == ButtonM3EStyle.outlined ||
|
||||
widget.style == ButtonM3EStyle.text) {
|
||||
return Colors.transparent;
|
||||
}
|
||||
return disabled ? color.withValues(alpha: .12) : color;
|
||||
});
|
||||
|
||||
final elevation = WidgetStateProperty.resolveWith<double>((states) {
|
||||
return tokens.elevation(widget.style, states);
|
||||
});
|
||||
|
||||
final shape = WidgetStateProperty.resolveWith<OutlinedBorder>((states) {
|
||||
return _shapeFor(states, tokens);
|
||||
});
|
||||
|
||||
return ButtonStyle(
|
||||
minimumSize: WidgetStateProperty.all(Size(48, m.height)),
|
||||
padding:
|
||||
WidgetStateProperty.all(EdgeInsets.symmetric(horizontal: m.hPadding)),
|
||||
foregroundColor: fg,
|
||||
backgroundColor: bg,
|
||||
shape: shape,
|
||||
elevation: elevation,
|
||||
animationDuration: const Duration(milliseconds: 140),
|
||||
visualDensity: VisualDensity.standard,
|
||||
splashFactory: InkRipple.splashFactory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_design/m3e_design.dart';
|
||||
|
||||
class ButtonM3EWidget extends StatelessWidget {
|
||||
const ButtonM3EWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final m3e = context.m3e;
|
||||
return Container(
|
||||
padding: EdgeInsets.all(m3e.spacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: m3e.colors.surfaceStrong,
|
||||
borderRadius: m3e.shapes.square.md,
|
||||
),
|
||||
child: Text('Button placeholder', style: m3e.typography.base.labelLarge),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _pascal(String s) => s
|
||||
.split('_')
|
||||
.map((p) => p.isEmpty ? '' : (p[0].toUpperCase() + p.substring(1)))
|
||||
.join();
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_design/m3e_design.dart';
|
||||
|
||||
import 'enums.dart';
|
||||
|
||||
@immutable
|
||||
class _ButtonMetrics {
|
||||
final double heightSmall;
|
||||
final double heightMedium;
|
||||
final double heightLarge;
|
||||
final EdgeInsetsGeometry paddingSmall;
|
||||
final EdgeInsetsGeometry paddingMedium;
|
||||
final EdgeInsetsGeometry paddingLarge;
|
||||
final BorderSide outlinedBorder;
|
||||
final double elevation;
|
||||
const _ButtonMetrics({
|
||||
required this.heightSmall,
|
||||
required this.heightMedium,
|
||||
required this.heightLarge,
|
||||
required this.paddingSmall,
|
||||
required this.paddingMedium,
|
||||
required this.paddingLarge,
|
||||
required this.outlinedBorder,
|
||||
required this.elevation,
|
||||
});
|
||||
}
|
||||
|
||||
_ButtonMetrics _metricsFor(BuildContext context, ButtonM3EDensity density) {
|
||||
final theme = Theme.of(context);
|
||||
final m3e =
|
||||
theme.extension<M3ETheme>() ?? M3ETheme.defaults(theme.colorScheme);
|
||||
final sp = m3e.spacing;
|
||||
|
||||
// Heights based on Material 3 expectations; tweakable by density.
|
||||
double hS = 36;
|
||||
double hM = 40;
|
||||
double hL = 48;
|
||||
|
||||
if (density == ButtonM3EDensity.compact) {
|
||||
hS -= 4;
|
||||
hM -= 4;
|
||||
hL -= 4;
|
||||
}
|
||||
|
||||
return _ButtonMetrics(
|
||||
heightSmall: hS,
|
||||
heightMedium: hM,
|
||||
heightLarge: hL,
|
||||
paddingSmall: EdgeInsets.symmetric(horizontal: sp.sm),
|
||||
paddingMedium: EdgeInsets.symmetric(horizontal: sp.md),
|
||||
paddingLarge: EdgeInsets.symmetric(horizontal: sp.lg),
|
||||
outlinedBorder: BorderSide(color: m3e.colors.outline, width: 1.0),
|
||||
elevation: 1.0,
|
||||
);
|
||||
}
|
||||
|
||||
class ButtonTokensAdapter {
|
||||
ButtonTokensAdapter(this.context);
|
||||
final BuildContext context;
|
||||
|
||||
M3ETheme get _m3e {
|
||||
final t = Theme.of(context);
|
||||
return t.extension<M3ETheme>() ?? M3ETheme.defaults(t.colorScheme);
|
||||
}
|
||||
|
||||
// Colors
|
||||
Color bgFilled() => _m3e.colors.primary;
|
||||
Color fgFilled() => _m3e.colors.onPrimary;
|
||||
Color bgTonal() => _m3e.colors.secondaryContainer;
|
||||
Color fgTonal() => _m3e.colors.onSecondaryContainer;
|
||||
Color bgElevated() => _m3e.colors.surfaceContainerLowest;
|
||||
Color fgElevated() => _m3e.colors.primary;
|
||||
Color fgText() => _m3e.colors.primary;
|
||||
Color borderOutlined() => _m3e.colors.outline;
|
||||
Color fgOutlined() => _m3e.colors.primary;
|
||||
Color disabledFg() => _m3e.colors.onSurface.withValues(alpha: 0.38);
|
||||
Color disabledBg() => _m3e.colors.onSurface.withValues(alpha: 0.12);
|
||||
|
||||
// Typography
|
||||
TextStyle labelStyle() => _m3e.type.labelLarge;
|
||||
|
||||
// Shapes
|
||||
OutlinedBorder shape(ButtonM3EShapeFamily family) {
|
||||
if (family == ButtonM3EShapeFamily.round) {
|
||||
return RoundedRectangleBorder(borderRadius: _m3e.shapes.round.lg);
|
||||
}
|
||||
// Square family should have sharp corners (no rounding)
|
||||
return const RoundedRectangleBorder(borderRadius: BorderRadius.zero);
|
||||
}
|
||||
|
||||
// Spacing & heights
|
||||
_ButtonMetrics metrics(ButtonM3EDensity density) =>
|
||||
_metricsFor(context, density);
|
||||
}
|
||||
115
packages/button_m3e/lib/src/button_tokens_adapter.dart
Normal file
115
packages/button_m3e/lib/src/button_tokens_adapter.dart
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_design/m3e_design.dart';
|
||||
import 'enums.dart';
|
||||
|
||||
@immutable
|
||||
class ButtonMeasurements {
|
||||
const ButtonMeasurements({
|
||||
required this.height,
|
||||
required this.hPadding,
|
||||
required this.iconSize,
|
||||
required this.iconGap,
|
||||
});
|
||||
final double height;
|
||||
final double hPadding;
|
||||
final double iconSize;
|
||||
final double iconGap;
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ButtonTokensAdapter {
|
||||
const ButtonTokensAdapter(this.context, {this.smallPaddingDeprecated24 = false});
|
||||
final BuildContext context;
|
||||
final bool smallPaddingDeprecated24;
|
||||
|
||||
M3ETheme get _m3e {
|
||||
final t = Theme.of(context);
|
||||
return t.extension<M3ETheme>() ?? M3ETheme.defaults(t.colorScheme);
|
||||
}
|
||||
|
||||
Color container(ButtonM3EStyle style) {
|
||||
final c = _m3e.colors;
|
||||
switch (style) {
|
||||
case ButtonM3EStyle.filled:
|
||||
return c.primary;
|
||||
case ButtonM3EStyle.tonal:
|
||||
return c.secondaryContainer;
|
||||
case ButtonM3EStyle.elevated:
|
||||
return c.surface;
|
||||
case ButtonM3EStyle.outlined:
|
||||
case ButtonM3EStyle.text:
|
||||
return Colors.transparent;
|
||||
}
|
||||
}
|
||||
|
||||
Color foreground(ButtonM3EStyle style) {
|
||||
final c = _m3e.colors;
|
||||
switch (style) {
|
||||
case ButtonM3EStyle.filled:
|
||||
return c.onPrimary;
|
||||
case ButtonM3EStyle.tonal:
|
||||
return c.onSecondaryContainer;
|
||||
case ButtonM3EStyle.elevated:
|
||||
case ButtonM3EStyle.outlined:
|
||||
case ButtonM3EStyle.text:
|
||||
return c.primary;
|
||||
}
|
||||
}
|
||||
|
||||
Color outline() => _m3e.colors.outline;
|
||||
|
||||
double elevation(ButtonM3EStyle style, Set<MaterialState> states) {
|
||||
final hovered = states.contains(MaterialState.hovered);
|
||||
final pressed = states.contains(MaterialState.pressed);
|
||||
final disabled = states.contains(MaterialState.disabled);
|
||||
if (disabled) return 0;
|
||||
switch (style) {
|
||||
case ButtonM3EStyle.elevated:
|
||||
return pressed ? 0 : hovered ? 3 : 1;
|
||||
case ButtonM3EStyle.filled:
|
||||
case ButtonM3EStyle.tonal:
|
||||
return pressed ? 0 : hovered ? 1 : 0;
|
||||
case ButtonM3EStyle.outlined:
|
||||
case ButtonM3EStyle.text:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
double squareRadius(ButtonM3ESize size) {
|
||||
switch (size) {
|
||||
case ButtonM3ESize.xs:
|
||||
return 8;
|
||||
case ButtonM3ESize.sm:
|
||||
return 8;
|
||||
case ButtonM3ESize.md:
|
||||
return 12;
|
||||
case ButtonM3ESize.lg:
|
||||
return 16;
|
||||
case ButtonM3ESize.xl:
|
||||
return 20;
|
||||
}
|
||||
}
|
||||
|
||||
double pressedRadius(ButtonM3ESize size) => (squareRadius(size) * 0.6).clamp(6, 18);
|
||||
|
||||
ButtonMeasurements measurements(ButtonM3ESize size) {
|
||||
switch (size) {
|
||||
case ButtonM3ESize.xs:
|
||||
return const ButtonMeasurements(height: 32, hPadding: 12, iconSize: 20, iconGap: 4);
|
||||
case ButtonM3ESize.sm:
|
||||
return ButtonMeasurements(
|
||||
height: 40,
|
||||
hPadding: smallPaddingDeprecated24 ? 24 : 16,
|
||||
iconSize: 20,
|
||||
iconGap: 8,
|
||||
);
|
||||
case ButtonM3ESize.md:
|
||||
return const ButtonMeasurements(height: 56, hPadding: 24, iconSize: 24, iconGap: 8);
|
||||
case ButtonM3ESize.lg:
|
||||
return const ButtonMeasurements(height: 96, hPadding: 48, iconSize: 32, iconGap: 12);
|
||||
case ButtonM3ESize.xl:
|
||||
return const ButtonMeasurements(height: 136, hPadding: 64, iconSize: 40, iconGap: 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
enum ButtonM3EVariant { filled, tonal, outlined, text, elevated }
|
||||
enum ButtonM3ESize { small, medium, large }
|
||||
enum ButtonM3EShapeFamily { round, square }
|
||||
enum ButtonM3EDensity { regular, compact }
|
||||
|
||||
enum ButtonM3EStyle { elevated, filled, tonal, outlined, text }
|
||||
enum ButtonM3ESize { xs, sm, md, lg, xl }
|
||||
enum ButtonM3EShape { round, square }
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
|
||||
name: button_m3e
|
||||
description: Material 3 Expressive Buttons for Flutter (filled, tonal, outlined, text, elevated) with M3E tokens.
|
||||
description: Material 3 Expressive Buttons for Flutter with 5 styles, 5 sizes, round/square shapes, and toggle selection.
|
||||
version: 0.1.0
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=3.5.0 <4.0.0"
|
||||
flutter: ">=3.22.0"
|
||||
flutter: ">=3.19.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
# melos_managed_dependency_overrides: m3e_design
|
||||
dependency_overrides:
|
||||
m3e_design:
|
||||
path: ..\\m3e_design
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('placeholder', () {
|
||||
expect(1 + 1, 2);
|
||||
test('sanity', () {
|
||||
expect(2 + 2, 4);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Expressive emphasis tweaks layered on top of baseline M3 type.
|
||||
/// Keep line-height the same; only tune weight/letter-spacing for emphasis.
|
||||
@immutable
|
||||
class M3EEmphasized {
|
||||
final TextStyle display;
|
||||
|
|
@ -14,12 +16,24 @@ class M3EEmphasized {
|
|||
required this.label,
|
||||
});
|
||||
|
||||
/// M3E guidance: slightly heavier weights and tighter tracking for large roles.
|
||||
static M3EEmphasized forBrightness(Brightness b) {
|
||||
// You could vary by brightness if desired; values below are neutral.
|
||||
return const M3EEmphasized(
|
||||
display: TextStyle(fontWeight: FontWeight.w800, letterSpacing: -0.5),
|
||||
headline: TextStyle(fontWeight: FontWeight.w700, letterSpacing: -0.25),
|
||||
title: TextStyle(fontWeight: FontWeight.w700),
|
||||
label: TextStyle(fontWeight: FontWeight.w700),
|
||||
display: TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.5, // subtle tightening on big sizes
|
||||
),
|
||||
headline: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.25,
|
||||
),
|
||||
title: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
label: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -32,6 +46,11 @@ class M3EEmphasized {
|
|||
);
|
||||
}
|
||||
|
||||
/// Material 3 Expressive typography tokens.
|
||||
/// - `base` starts from **M3 (Typography.material2021)**.
|
||||
/// - Optionally remaps fonts: brand (UI) for display/headline/title/label
|
||||
/// and plain (reading) for body.
|
||||
/// - Adds an emphasized set (weight/tracking tweaks) for expressive hierarchy.
|
||||
@immutable
|
||||
class M3ETypography {
|
||||
final TextTheme base;
|
||||
|
|
@ -39,16 +58,73 @@ class M3ETypography {
|
|||
|
||||
const M3ETypography({required this.base, required this.emphasized});
|
||||
|
||||
factory M3ETypography.defaultFor(Brightness b) {
|
||||
// Use a minimal baseline; app's ThemeData will provide fuller TextTheme.
|
||||
const textTheme = TextTheme();
|
||||
/// Build default M3E typography from M3.
|
||||
///
|
||||
/// [brandFontFamily] is typically your UI/brand face (e.g., Roboto Flex).
|
||||
/// [plainFontFamily] is typically your reading face (e.g., Roboto Serif).
|
||||
/// If you pass neither, you’ll get pure M3 defaults (no family swap),
|
||||
/// but still keep the M3E emphasized set for optional use.
|
||||
factory M3ETypography.defaultFor(
|
||||
Brightness brightness, {
|
||||
String? brandFontFamily,
|
||||
String? plainFontFamily,
|
||||
TextTheme? baseOverride,
|
||||
}) {
|
||||
// 1) Start from Material 3 baseline type.
|
||||
final m3 = Typography.material2021();
|
||||
final TextTheme m3Base =
|
||||
baseOverride ?? (brightness == Brightness.dark ? m3.white : m3.black);
|
||||
|
||||
// 2) Optionally map brand/plain families to role groups (M3E guidance).
|
||||
final TextTheme baseWithFamilies = _applyFamilies(
|
||||
m3Base,
|
||||
brand: brandFontFamily,
|
||||
plain: plainFontFamily,
|
||||
);
|
||||
|
||||
// 3) Provide emphasized deltas (weights/tracking).
|
||||
return M3ETypography(
|
||||
base: textTheme, emphasized: M3EEmphasized.forBrightness(b));
|
||||
base: baseWithFamilies,
|
||||
emphasized: M3EEmphasized.forBrightness(brightness),
|
||||
);
|
||||
}
|
||||
|
||||
/// Lerp the full token set.
|
||||
static M3ETypography lerp(M3ETypography a, M3ETypography b, double t) =>
|
||||
M3ETypography(
|
||||
base: TextTheme.lerp(a.base, b.base, t),
|
||||
emphasized: M3EEmphasized.lerp(a.emphasized, b.emphasized, t),
|
||||
);
|
||||
|
||||
/// Apply brand/plain families: brand → display/headline/title/label,
|
||||
/// plain → body. If a family is null, keep the original.
|
||||
static TextTheme _applyFamilies(
|
||||
TextTheme t, {
|
||||
String? brand,
|
||||
String? plain,
|
||||
}) {
|
||||
TextStyle? _withFam(TextStyle? s, String? fam) =>
|
||||
fam == null ? s : s?.copyWith(fontFamily: fam);
|
||||
|
||||
return t.copyWith(
|
||||
// Brand / UI voice
|
||||
displayLarge: _withFam(t.displayLarge, brand),
|
||||
displayMedium: _withFam(t.displayMedium, brand),
|
||||
displaySmall: _withFam(t.displaySmall, brand),
|
||||
headlineLarge: _withFam(t.headlineLarge, brand),
|
||||
headlineMedium: _withFam(t.headlineMedium, brand),
|
||||
headlineSmall: _withFam(t.headlineSmall, brand),
|
||||
titleLarge: _withFam(t.titleLarge, brand),
|
||||
titleMedium: _withFam(t.titleMedium, brand),
|
||||
titleSmall: _withFam(t.titleSmall, brand),
|
||||
labelLarge: _withFam(t.labelLarge, brand),
|
||||
labelMedium: _withFam(t.labelMedium, brand),
|
||||
labelSmall: _withFam(t.labelSmall, brand),
|
||||
|
||||
// Reading voice
|
||||
bodyLarge: _withFam(t.bodyLarge, plain),
|
||||
bodyMedium: _withFam(t.bodyMedium, plain),
|
||||
bodySmall: _withFam(t.bodySmall, plain),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue