diff --git a/apps/gallery/lib/main.dart b/apps/gallery/lib/main.dart index e7a59a0..5c91221 100644 --- a/apps/gallery/lib/main.dart +++ b/apps/gallery/lib/main.dart @@ -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 { - // 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 { 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( - label: 'Primary', - onPressed: () {}, - items: const [ - SplitButtonM3EItem(value: 'one', child: 'One'), - SplitButtonM3EItem(value: 'two', child: 'Two'), - ], - ), - SplitButtonM3E( - label: 'Tonal', - emphasis: SplitButtonM3EEmphasis.tonal, - onPressed: () {}, - items: const [ - SplitButtonM3EItem(value: 'one', child: 'One'), - SplitButtonM3EItem(value: 'two', child: 'Two'), - ], - ), - SplitButtonM3E( - label: 'Outlined', - emphasis: SplitButtonM3EEmphasis.outlined, - onPressed: () {}, - items: const [ - SplitButtonM3EItem(value: 'one', child: 'One'), - SplitButtonM3EItem(value: 'two', child: 'Two'), - ], - ), - SplitButtonM3E( - label: 'Text', - emphasis: SplitButtonM3EEmphasis.text, - onPressed: () {}, - items: const [ - SplitButtonM3EItem(value: 'one', child: 'One'), - SplitButtonM3EItem(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( - label: 'Primary', - onPressed: () {}, - items: const [ - SplitButtonM3EItem(value: 'one', child: 'One'), - SplitButtonM3EItem(value: 'two', child: 'Two'), - ], - ), - SplitButtonM3E( - label: 'Tonal', - emphasis: SplitButtonM3EEmphasis.tonal, - onPressed: () {}, - items: const [ - SplitButtonM3EItem(value: 'one', child: 'One'), - SplitButtonM3EItem(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()), + ), + ), + ), + ), + ], + ); + } +} diff --git a/apps/gallery/lib/sections/button_section.dart b/apps/gallery/lib/sections/button_section.dart new file mode 100644 index 0000000..c26056f --- /dev/null +++ b/apps/gallery/lib/sections/button_section.dart @@ -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: () {}, + ), + ], + ), + ], + ], + ], + ), + ); + } +} diff --git a/apps/gallery/lib/sections/fab_section.dart b/apps/gallery/lib/sections/fab_section.dart new file mode 100644 index 0000000..ebfaf76 --- /dev/null +++ b/apps/gallery/lib/sections/fab_section.dart @@ -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), + ], + ), + ], + ), + ); + } +} diff --git a/apps/gallery/lib/sections/icon_button_section.dart b/apps/gallery/lib/sections/icon_button_section.dart new file mode 100644 index 0000000..991abad --- /dev/null +++ b/apps/gallery/lib/sections/icon_button_section.dart @@ -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: () {}, + ), + ], + ), + ], + ], + ), + ); + } +} diff --git a/apps/gallery/lib/sections/loading_indicator_section.dart b/apps/gallery/lib/sections/loading_indicator_section.dart new file mode 100644 index 0000000..78a6102 --- /dev/null +++ b/apps/gallery/lib/sections/loading_indicator_section.dart @@ -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), + ], + ), + ); + } +} diff --git a/apps/gallery/lib/sections/navigation_section.dart b/apps/gallery/lib/sections/navigation_section.dart new file mode 100644 index 0000000..1498f1a --- /dev/null +++ b/apps/gallery/lib/sections/navigation_section.dart @@ -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 createState() => _NavigationSectionState(); +} + +class _NavigationSectionState extends State { + int _barIndex = 0; + int _railIndex = 0; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final m3e = theme.extension() ?? 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')), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/apps/gallery/lib/sections/progress_section.dart b/apps/gallery/lib/sections/progress_section.dart new file mode 100644 index 0000000..78be099 --- /dev/null +++ b/apps/gallery/lib/sections/progress_section.dart @@ -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, + ), + ], + ), + ], + ), + ); + } +} diff --git a/apps/gallery/lib/sections/section_card.dart b/apps/gallery/lib/sections/section_card.dart new file mode 100644 index 0000000..10f1b5b --- /dev/null +++ b/apps/gallery/lib/sections/section_card.dart @@ -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.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, + ), + ), + ], + ), + ); + } +} diff --git a/apps/gallery/lib/sections/slider_section.dart b/apps/gallery/lib/sections/slider_section.dart new file mode 100644 index 0000000..ee8e1d7 --- /dev/null +++ b/apps/gallery/lib/sections/slider_section.dart @@ -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 createState() => _SliderSectionState(); +} + +class _SliderSectionState extends State { + 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), + ), + ), + ], + ), + ), + ], + ), + ], + ], + ), + ); + } +} diff --git a/apps/gallery/lib/sections/split_button_section.dart b/apps/gallery/lib/sections/split_button_section.dart new file mode 100644 index 0000000..829a713 --- /dev/null +++ b/apps/gallery/lib/sections/split_button_section.dart @@ -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(value: 'one', child: 'One'), + SplitButtonM3EItem(value: 'two', child: 'Two'), + SplitButtonM3EItem(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( + label: emphasis.name, + size: size, + emphasis: emphasis, + onPressed: () {}, + items: items, + ), + ], + ), + ], + ], + ), + ); + } +} diff --git a/apps/gallery/lib/sections/toolbar_section.dart b/apps/gallery/lib/sections/toolbar_section.dart new file mode 100644 index 0000000..582459b --- /dev/null +++ b/apps/gallery/lib/sections/toolbar_section.dart @@ -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, + ), + ], + ), + ], + ], + ), + ); + } +} diff --git a/packages/button_m3e/LICENSE b/packages/button_m3e/LICENSE index 12ca7c2..df278d3 100644 --- a/packages/button_m3e/LICENSE +++ b/packages/button_m3e/LICENSE @@ -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. +MIT License \ No newline at end of file diff --git a/packages/button_m3e/README.md b/packages/button_m3e/README.md index 315c194..148ec00 100644 --- a/packages/button_m3e/README.md +++ b/packages/button_m3e/README.md @@ -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. diff --git a/packages/button_m3e/lib/button_m3e.dart b/packages/button_m3e/lib/button_m3e.dart index 0a55c55..a1d7724 100644 --- a/packages/button_m3e/lib/button_m3e.dart +++ b/packages/button_m3e/lib/button_m3e.dart @@ -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'; diff --git a/packages/button_m3e/lib/src/button_m3e.dart b/packages/button_m3e/lib/src/button_m3e.dart index 290aa67..a613680 100644 --- a/packages/button_m3e/lib/src/button_m3e.dart +++ b/packages/button_m3e/lib/src/button_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? onSelectedChange; + final bool smallPaddingDeprecated24; + final bool enabled; + final WidgetStatesController? statesController; + + @override + State createState() => _ButtonM3EState(); +} + +class _ButtonM3EState extends State { + 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 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((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((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((states) { + return tokens.elevation(widget.style, states); + }); + + final shape = WidgetStateProperty.resolveWith((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, + ); + } } diff --git a/packages/button_m3e/lib/src/button_m3e_widget.dart b/packages/button_m3e/lib/src/button_m3e_widget.dart deleted file mode 100644 index dab3275..0000000 --- a/packages/button_m3e/lib/src/button_m3e_widget.dart +++ /dev/null @@ -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(); diff --git a/packages/button_m3e/lib/src/button_theme_m3e.dart b/packages/button_m3e/lib/src/button_theme_m3e.dart deleted file mode 100644 index 0664324..0000000 --- a/packages/button_m3e/lib/src/button_theme_m3e.dart +++ /dev/null @@ -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.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.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); -} diff --git a/packages/button_m3e/lib/src/button_tokens_adapter.dart b/packages/button_m3e/lib/src/button_tokens_adapter.dart new file mode 100644 index 0000000..299fb13 --- /dev/null +++ b/packages/button_m3e/lib/src/button_tokens_adapter.dart @@ -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.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 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); + } + } +} diff --git a/packages/button_m3e/lib/src/enums.dart b/packages/button_m3e/lib/src/enums.dart index e46edcd..d443696 100644 --- a/packages/button_m3e/lib/src/enums.dart +++ b/packages/button_m3e/lib/src/enums.dart @@ -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 } diff --git a/packages/button_m3e/pubspec.yaml b/packages/button_m3e/pubspec.yaml index 8313f16..21f9363 100644 --- a/packages/button_m3e/pubspec.yaml +++ b/packages/button_m3e/pubspec.yaml @@ -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: diff --git a/packages/button_m3e/pubspec_overrides.yaml b/packages/button_m3e/pubspec_overrides.yaml deleted file mode 100644 index 69acba6..0000000 --- a/packages/button_m3e/pubspec_overrides.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# melos_managed_dependency_overrides: m3e_design -dependency_overrides: - m3e_design: - path: ..\\m3e_design diff --git a/packages/button_m3e/test/button_m3e_test.dart b/packages/button_m3e/test/button_m3e_test.dart index 83da334..7668876 100644 --- a/packages/button_m3e/test/button_m3e_test.dart +++ b/packages/button_m3e/test/button_m3e_test.dart @@ -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); }); } diff --git a/packages/m3e_design/lib/tokens/typography_tokens.dart b/packages/m3e_design/lib/tokens/typography_tokens.dart index fe23136..63a5ad3 100644 --- a/packages/m3e_design/lib/tokens/typography_tokens.dart +++ b/packages/m3e_design/lib/tokens/typography_tokens.dart @@ -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), + ); + } }