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
7
packages/navigation_bar_m3e/lib/navigation_bar_m3e.dart
Normal file
7
packages/navigation_bar_m3e/lib/navigation_bar_m3e.dart
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
library navigation_bar_m3e;
|
||||
|
||||
export 'src/enums.dart';
|
||||
export 'src/nav_tokens_adapter.dart' show NavTokensAdapter;
|
||||
export 'src/navigation_bar_m3e.dart';
|
||||
export 'src/nav_badge_m3e.dart';
|
||||
export 'src/nav_destination_m3e.dart';
|
||||
5
packages/navigation_bar_m3e/lib/src/enums.dart
Normal file
5
packages/navigation_bar_m3e/lib/src/enums.dart
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
enum NavBarM3ELabelBehavior { alwaysShow, onlySelected, alwaysHide }
|
||||
enum NavBarM3ESize { small, medium }
|
||||
enum NavBarM3EShapeFamily { round, square }
|
||||
enum NavBarM3EDensity { regular, compact }
|
||||
enum NavBarM3EIndicatorStyle { pill, underline, none }
|
||||
78
packages/navigation_bar_m3e/lib/src/nav_badge_m3e.dart
Normal file
78
packages/navigation_bar_m3e/lib/src/nav_badge_m3e.dart
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_design/m3e_design.dart';
|
||||
|
||||
class NavBadgeM3E extends StatelessWidget {
|
||||
const NavBadgeM3E({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.count,
|
||||
this.showDot = false,
|
||||
this.maxCount = 99,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
this.semanticLabel,
|
||||
this.offset = const Offset(8, -6),
|
||||
}) : assert(count == null || count >= 0);
|
||||
|
||||
final Widget child;
|
||||
final int? count;
|
||||
final bool showDot;
|
||||
final int maxCount;
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
final String? semanticLabel;
|
||||
final Offset offset;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Theme.of(context);
|
||||
final m3e = t.extension<M3ETheme>() ?? M3ETheme.defaults(t.colorScheme);
|
||||
final bg = backgroundColor ?? m3e.colors.errorContainer;
|
||||
final fg = foregroundColor ?? m3e.colors.onErrorContainer;
|
||||
|
||||
final badge = showDot
|
||||
? _dot(bg)
|
||||
: _label(bg, fg, count == null ? '' : _format(count!, maxCount));
|
||||
|
||||
final stack = Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
child,
|
||||
Positioned(
|
||||
right: offset.dx,
|
||||
top: offset.dy,
|
||||
child: Semantics(
|
||||
label: semanticLabel ?? (count != null ? 'Notifications: ${count!}' : 'Notifications'),
|
||||
child: badge,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return stack;
|
||||
}
|
||||
|
||||
Widget _dot(Color bg) {
|
||||
return Container(
|
||||
width: 8, height: 8,
|
||||
decoration: BoxDecoration(color: bg, shape: BoxShape.circle),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _label(Color bg, Color fg, String text) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
constraints: const BoxConstraints(minWidth: 18, minHeight: 18),
|
||||
child: DefaultTextStyle(
|
||||
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w600),
|
||||
child: Text(text, textAlign: TextAlign.center, style: TextStyle(color: fg)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _format(int c, int max) => (c > max) ? '$max+' : '$c';
|
||||
}
|
||||
38
packages/navigation_bar_m3e/lib/src/nav_destination_m3e.dart
Normal file
38
packages/navigation_bar_m3e/lib/src/nav_destination_m3e.dart
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'nav_badge_m3e.dart';
|
||||
|
||||
class NavigationDestinationM3E {
|
||||
const NavigationDestinationM3E({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
this.selectedIcon,
|
||||
this.badgeCount,
|
||||
this.badgeDot = false,
|
||||
this.semanticLabel,
|
||||
});
|
||||
|
||||
final Widget icon;
|
||||
final Widget? selectedIcon;
|
||||
final String label;
|
||||
|
||||
/// Optional badge counter
|
||||
final int? badgeCount;
|
||||
|
||||
/// If true, show a small dot instead of a counter.
|
||||
final bool badgeDot;
|
||||
|
||||
final String? semanticLabel;
|
||||
|
||||
Widget buildIcon([bool selected = false]) {
|
||||
final base = selected && selectedIcon != null ? selectedIcon! : icon;
|
||||
if (badgeCount != null || badgeDot) {
|
||||
return NavBadgeM3E(
|
||||
child: base,
|
||||
count: badgeCount,
|
||||
showDot: badgeDot,
|
||||
semanticLabel: semanticLabel,
|
||||
);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
}
|
||||
84
packages/navigation_bar_m3e/lib/src/nav_tokens_adapter.dart
Normal file
84
packages/navigation_bar_m3e/lib/src/nav_tokens_adapter.dart
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_design/m3e_design.dart';
|
||||
import 'enums.dart';
|
||||
|
||||
@immutable
|
||||
class _NavMetrics {
|
||||
final double heightSmall;
|
||||
final double heightMedium;
|
||||
final double iconSize;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final double indicatorThickness; // for underline
|
||||
const _NavMetrics({
|
||||
required this.heightSmall,
|
||||
required this.heightMedium,
|
||||
required this.iconSize,
|
||||
required this.padding,
|
||||
required this.indicatorThickness,
|
||||
});
|
||||
}
|
||||
|
||||
_NavMetrics _metricsFor(BuildContext context, NavBarM3EDensity density) {
|
||||
final theme = Theme.of(context);
|
||||
final m3e = theme.extension<M3ETheme>() ?? M3ETheme.defaults(theme.colorScheme);
|
||||
final sp = m3e.spacing;
|
||||
|
||||
double hSmall = 64; // compact/phone-tight
|
||||
double hMedium = 80; // default M3 nav bar height
|
||||
double icon = 24;
|
||||
double underline = 3;
|
||||
|
||||
if (density == NavBarM3EDensity.compact) {
|
||||
hSmall -= 4; hMedium -= 4; underline -= 1;
|
||||
}
|
||||
|
||||
return _NavMetrics(
|
||||
heightSmall: hSmall,
|
||||
heightMedium: hMedium,
|
||||
iconSize: icon,
|
||||
padding: EdgeInsets.symmetric(horizontal: sp.md),
|
||||
indicatorThickness: underline,
|
||||
);
|
||||
}
|
||||
|
||||
class NavTokensAdapter {
|
||||
NavTokensAdapter(this.context);
|
||||
final BuildContext context;
|
||||
|
||||
M3ETheme get _m3e {
|
||||
final t = Theme.of(context);
|
||||
return t.extension<M3ETheme>() ?? M3ETheme.defaults(t.colorScheme);
|
||||
}
|
||||
|
||||
_NavMetrics metrics(NavBarM3EDensity density) => _metricsFor(context, density);
|
||||
|
||||
// Container/background
|
||||
Color containerColor() => _m3e.colors.surfaceContainerHigh;
|
||||
|
||||
// Indicator
|
||||
Color indicatorColor() => _m3e.colors.secondaryContainer;
|
||||
|
||||
// Icon/label colors
|
||||
Color selectedColor() => _m3e.colors.onSecondaryContainer;
|
||||
Color unselectedColor() => _m3e.colors.onSurfaceVariant;
|
||||
|
||||
// Typography
|
||||
TextStyle labelStyle() => _m3e.type.labelMedium;
|
||||
|
||||
// Shapes
|
||||
ShapeBorder containerShape(NavBarM3EShapeFamily family) {
|
||||
final set = family == NavBarM3EShapeFamily.round ? _m3e.shapes.round : _m3e.shapes.square;
|
||||
return RoundedRectangleBorder(borderRadius: set.lg);
|
||||
}
|
||||
|
||||
ShapeBorder indicatorShapePill() => const StadiumBorder();
|
||||
|
||||
// Underline decoration for selected.
|
||||
BoxDecoration underlineDecoration(Color color, double thickness) {
|
||||
return BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: color, width: thickness),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
144
packages/navigation_bar_m3e/lib/src/navigation_bar_m3e.dart
Normal file
144
packages/navigation_bar_m3e/lib/src/navigation_bar_m3e.dart
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_design/m3e_design.dart';
|
||||
import 'enums.dart';
|
||||
import 'nav_tokens_adapter.dart';
|
||||
import 'nav_destination_m3e.dart';
|
||||
|
||||
class NavigationBarM3E extends StatelessWidget {
|
||||
const NavigationBarM3E({
|
||||
super.key,
|
||||
required this.destinations,
|
||||
this.selectedIndex = 0,
|
||||
this.onDestinationSelected,
|
||||
this.labelBehavior = NavBarM3ELabelBehavior.onlySelected,
|
||||
this.size = NavBarM3ESize.medium,
|
||||
this.shapeFamily = NavBarM3EShapeFamily.round,
|
||||
this.density = NavBarM3EDensity.regular,
|
||||
this.backgroundColor,
|
||||
this.elevation,
|
||||
this.indicatorStyle = NavBarM3EIndicatorStyle.pill,
|
||||
this.indicatorColor,
|
||||
this.padding,
|
||||
this.safeArea = true,
|
||||
this.semanticLabel,
|
||||
});
|
||||
|
||||
final List<NavigationDestinationM3E> destinations;
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int>? onDestinationSelected;
|
||||
|
||||
final NavBarM3ELabelBehavior labelBehavior;
|
||||
final NavBarM3ESize size;
|
||||
final NavBarM3EShapeFamily shapeFamily;
|
||||
final NavBarM3EDensity density;
|
||||
|
||||
final Color? backgroundColor;
|
||||
final double? elevation;
|
||||
|
||||
final NavBarM3EIndicatorStyle indicatorStyle;
|
||||
final Color? indicatorColor;
|
||||
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final bool safeArea;
|
||||
|
||||
final String? semanticLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(destinations.isNotEmpty, 'Provide at least one destination');
|
||||
|
||||
final tokens = NavTokensAdapter(context);
|
||||
final metrics = tokens.metrics(density);
|
||||
final m3e = Theme.of(context).extension<M3ETheme>() ?? M3ETheme.defaults(Theme.of(context).colorScheme);
|
||||
|
||||
final height = size == NavBarM3ESize.small ? metrics.heightSmall : metrics.heightMedium;
|
||||
final bg = backgroundColor ?? tokens.containerColor();
|
||||
final shape = tokens.containerShape(shapeFamily);
|
||||
|
||||
final nav = Material(
|
||||
color: bg,
|
||||
elevation: elevation ?? 0,
|
||||
shape: shape,
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
child: NavigationBar(
|
||||
height: height,
|
||||
elevation: elevation ?? 0,
|
||||
indicatorColor: indicatorStyle == NavBarM3EIndicatorStyle.none
|
||||
? Colors.transparent
|
||||
: (indicatorColor ?? tokens.indicatorColor()),
|
||||
indicatorShape: switch (indicatorStyle) {
|
||||
NavBarM3EIndicatorStyle.pill => tokens.indicatorShapePill(),
|
||||
NavBarM3EIndicatorStyle.underline => const StadiumBorder(), // we'll fake underline via decoration below
|
||||
NavBarM3EIndicatorStyle.none => const StadiumBorder(),
|
||||
},
|
||||
backgroundColor: Colors.transparent, // outer Material supplies bg + shape
|
||||
labelBehavior: switch (labelBehavior) {
|
||||
NavBarM3ELabelBehavior.alwaysShow => NavigationDestinationLabelBehavior.alwaysShow,
|
||||
NavBarM3ELabelBehavior.onlySelected => NavigationDestinationLabelBehavior.onlyShowSelected,
|
||||
NavBarM3ELabelBehavior.alwaysHide => NavigationDestinationLabelBehavior.alwaysHide,
|
||||
},
|
||||
selectedIndex: selectedIndex,
|
||||
destinations: List.generate(destinations.length, (i) {
|
||||
final d = destinations[i];
|
||||
return NavigationDestination(
|
||||
icon: _icon(context, false, d, metrics.iconSize),
|
||||
selectedIcon: _selectedIcon(context, true, d, metrics.iconSize, tokens, indicatorStyle),
|
||||
label: d.label,
|
||||
tooltip: d.semanticLabel,
|
||||
);
|
||||
}),
|
||||
onDestinationSelected: onDestinationSelected,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final padded = Padding(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
child: nav,
|
||||
);
|
||||
|
||||
final content = DefaultTextStyle.merge(
|
||||
style: tokens.labelStyle().copyWith(
|
||||
color: m3e.colors.onSurfaceVariant,
|
||||
),
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(size: metrics.iconSize, color: m3e.colors.onSurfaceVariant),
|
||||
child: padded,
|
||||
),
|
||||
);
|
||||
|
||||
if (!safeArea && semanticLabel == null) return content;
|
||||
final wrapped = SafeArea(top: false, left: false, right: false, bottom: safeArea, child: content);
|
||||
|
||||
if (semanticLabel == null) return wrapped;
|
||||
return Semantics(container: true, label: semanticLabel!, child: wrapped);
|
||||
}
|
||||
|
||||
Widget _icon(BuildContext context, bool selected, NavigationDestinationM3E d, double iconSize) {
|
||||
return SizedBox(
|
||||
width: iconSize + 8, // give a little space for underline
|
||||
height: iconSize + 8,
|
||||
child: Center(child: d.buildIcon(selected)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _selectedIcon(
|
||||
BuildContext context,
|
||||
bool selected,
|
||||
NavigationDestinationM3E d,
|
||||
double iconSize,
|
||||
NavTokensAdapter tokens,
|
||||
NavBarM3EIndicatorStyle style,
|
||||
) {
|
||||
final w = _icon(context, selected, d, iconSize);
|
||||
if (style != NavBarM3EIndicatorStyle.underline) return w;
|
||||
|
||||
final metrics = tokens.metrics(density);
|
||||
final deco = tokens.underlineDecoration(tokens.indicatorColor(), metrics.indicatorThickness);
|
||||
return DecoratedBox(
|
||||
decoration: deco,
|
||||
child: w,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_design/m3e_design.dart';
|
||||
|
||||
class NavigationBarM3EWidget extends StatelessWidget {
|
||||
const NavigationBarM3EWidget({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('NavigationBar placeholder', style: m3e.typography.base.titleMedium),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _pascal(String s) => s.split('_').map((p) => p.isEmpty ? '' : (p[0].toUpperCase() + p.substring(1))).join();
|
||||
Loading…
Add table
Add a link
Reference in a new issue