forked from mirrors/material_3_expressive
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:
parent
2c0f2df0b8
commit
62ecb86b76
184 changed files with 9872 additions and 0 deletions
21
packages/button_m3e/LICENSE
Normal file
21
packages/button_m3e/LICENSE
Normal 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.
|
||||
70
packages/button_m3e/README.md
Normal file
70
packages/button_m3e/README.md
Normal 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
|
||||
5
packages/button_m3e/lib/button_m3e.dart
Normal file
5
packages/button_m3e/lib/button_m3e.dart
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
library button_m3e;
|
||||
|
||||
export 'src/enums.dart';
|
||||
export 'src/button_m3e.dart';
|
||||
export 'src/button_theme_m3e.dart';
|
||||
144
packages/button_m3e/lib/src/button_m3e.dart
Normal file
144
packages/button_m3e/lib/src/button_m3e.dart
Normal 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!],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
24
packages/button_m3e/lib/src/button_m3e_widget.dart
Normal file
24
packages/button_m3e/lib/src/button_m3e_widget.dart
Normal 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();
|
||||
94
packages/button_m3e/lib/src/button_theme_m3e.dart
Normal file
94
packages/button_m3e/lib/src/button_theme_m3e.dart
Normal 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);
|
||||
}
|
||||
4
packages/button_m3e/lib/src/enums.dart
Normal file
4
packages/button_m3e/lib/src/enums.dart
Normal 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 }
|
||||
29
packages/button_m3e/melos_button_m3e.iml
Normal file
29
packages/button_m3e/melos_button_m3e.iml
Normal 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>
|
||||
18
packages/button_m3e/pubspec.yaml
Normal file
18
packages/button_m3e/pubspec.yaml
Normal 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
|
||||
4
packages/button_m3e/pubspec_overrides.yaml
Normal file
4
packages/button_m3e/pubspec_overrides.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# melos_managed_dependency_overrides: m3e_design
|
||||
dependency_overrides:
|
||||
m3e_design:
|
||||
path: ..\\m3e_design
|
||||
7
packages/button_m3e/test/button_m3e_test.dart
Normal file
7
packages/button_m3e/test/button_m3e_test.dart
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('placeholder', () {
|
||||
expect(1 + 1, 2);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue