Add initial configuration, tokens, and widgets for M3E components

- Introduced `.gitignore` and `.metadata` for apps and examples.
- Added Flutter/Dart analysis configurations (`analysis_options.yaml`).
- Implemented foundational tokens and themes for M3E (colors, shapes).
- Created base implementations for `IconButtonM3E` and `SplitButtonM3E`.
- Set up CI workflow (`ci.yaml`) to automate testing and analysis.
This commit is contained in:
Emily Pauli 2025-10-21 22:15:15 +02:00
commit 62ecb86b76
184 changed files with 9872 additions and 0 deletions

View file

@ -0,0 +1,21 @@
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.

View file

@ -0,0 +1,70 @@
# button_m3e
Material 3 **Expressive** Buttons for Flutter, built on top of Flutter's buttons but styled via **M3E** tokens.
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

View file

@ -0,0 +1,5 @@
library button_m3e;
export 'src/enums.dart';
export 'src/button_m3e.dart';
export 'src/button_theme_m3e.dart';

View file

@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'enums.dart';
import 'button_theme_m3e.dart';
class ButtonM3E extends StatelessWidget {
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');
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 ButtonM3ESize size;
final ButtonM3EShapeFamily shapeFamily;
final ButtonM3EDensity density;
final String? semanticLabel;
@override
Widget build(BuildContext context) {
final t = ButtonTokensAdapter(context);
final m = t.metrics(density);
final shape = t.shape(shapeFamily);
final (minH, pad) = switch (size) {
ButtonM3ESize.small => (m.heightSmall, m.paddingSmall),
ButtonM3ESize.medium => (m.heightMedium, m.paddingMedium),
ButtonM3ESize.large => (m.heightLarge, m.paddingLarge),
};
final style = _styleFor(context, t, shape, minH, pad);
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,
);
case ButtonM3EVariant.tonal:
return FilledButton.styleFrom(
backgroundColor: t.bgTonal(),
foregroundColor: t.fgTonal(),
textStyle: t.labelStyle(),
minimumSize: Size(0, minH),
padding: pad,
shape: shape,
);
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 ButtonM3EVariant.text:
return TextButton.styleFrom(
foregroundColor: t.fgText(),
textStyle: t.labelStyle(),
minimumSize: Size(0, minH),
padding: pad,
shape: shape,
);
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),
);
}
}
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;
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!],
],
);
}
}

View file

@ -0,0 +1,24 @@
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();

View file

@ -0,0 +1,94 @@
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);
}

View file

@ -0,0 +1,4 @@
enum ButtonM3EVariant { filled, tonal, outlined, text, elevated }
enum ButtonM3ESize { small, medium, large }
enum ButtonM3EShapeFamily { round, square }
enum ButtonM3EDensity { regular, compact }

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.idea" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/example/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/example/.idea" />
<excludeFolder url="file://$MODULE_DIR$/example/.pub" />
<excludeFolder url="file://$MODULE_DIR$/example/build" />
<excludeFolder url="file://$MODULE_DIR$/example/android/.gradle" />
<excludeFolder url="file://$MODULE_DIR$/example/android/.idea" />
<excludeFolder url="file://$MODULE_DIR$/example/ios/Flutter" />
<excludeFolder url="file://$MODULE_DIR$/example/ios/Pods" />
<excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks" />
<excludeFolder url="file://$MODULE_DIR$/example/macos/Flutter" />
<excludeFolder url="file://$MODULE_DIR$/example/macos/Pods" />
<excludeFolder url="file://$MODULE_DIR$/example/macos/.symlinks" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Flutter Plugins" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

View file

@ -0,0 +1,18 @@
name: button_m3e
description: Material 3 Expressive Buttons for Flutter (filled, tonal, outlined, text, elevated) with M3E tokens.
version: 0.1.0
publish_to: none
environment:
sdk: ">=3.5.0 <4.0.0"
flutter: ">=3.22.0"
dependencies:
flutter:
sdk: flutter
m3e_design:
path: ../m3e_design
dev_dependencies:
flutter_test:
sdk: flutter

View file

@ -0,0 +1,4 @@
# melos_managed_dependency_overrides: m3e_design
dependency_overrides:
m3e_design:
path: ..\\m3e_design

View file

@ -0,0 +1,7 @@
import 'package:flutter_test/flutter_test.dart';
void main() {
test('placeholder', () {
expect(1 + 1, 2);
});
}