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
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 }
|
||||
Loading…
Add table
Add a link
Reference in a new issue