Refactor NavigationRailM3E to simplify rail types; update badge handling and expand functionality of navigation components.

This commit is contained in:
Emily Pauli 2025-10-23 14:32:23 +02:00
commit 5b27a91894
20 changed files with 360 additions and 486 deletions

View file

@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'nav_badge_m3e.dart';
class NavigationDestinationM3E {
@ -15,7 +17,7 @@ class NavigationDestinationM3E {
final Widget? selectedIcon;
final String label;
/// Optional badge counter
/// Optional badgeValue counter
final int? badgeCount;
/// If true, show a small dot instead of a counter.

View file

@ -1,8 +1,9 @@
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';
import 'nav_tokens_adapter.dart';
class NavigationBarM3E extends StatelessWidget {
const NavigationBarM3E({
@ -10,7 +11,7 @@ class NavigationBarM3E extends StatelessWidget {
required this.destinations,
this.selectedIndex = 0,
this.onDestinationSelected,
this.labelBehavior = NavBarM3ELabelBehavior.onlySelected,
this.labelBehavior = NavBarM3ELabelBehavior.alwaysShow,
this.size = NavBarM3ESize.medium,
this.shapeFamily = NavBarM3EShapeFamily.round,
this.density = NavBarM3EDensity.regular,
@ -49,9 +50,12 @@ class NavigationBarM3E extends StatelessWidget {
final tokens = NavTokensAdapter(context);
final metrics = tokens.metrics(density);
final m3e = Theme.of(context).extension<M3ETheme>() ?? M3ETheme.defaults(Theme.of(context).colorScheme);
final m3e = Theme.of(context).extension<M3ETheme>() ??
M3ETheme.defaults(Theme.of(context).colorScheme);
final height = size == NavBarM3ESize.small ? metrics.heightSmall : metrics.heightMedium;
final height = size == NavBarM3ESize.small
? metrics.heightSmall
: metrics.heightMedium;
final bg = backgroundColor ?? tokens.containerColor();
final shape = tokens.containerShape(shapeFamily);
@ -69,21 +73,27 @@ class NavigationBarM3E extends StatelessWidget {
: (indicatorColor ?? tokens.indicatorColor()),
indicatorShape: switch (indicatorStyle) {
NavBarM3EIndicatorStyle.pill => tokens.indicatorShapePill(),
NavBarM3EIndicatorStyle.underline => const StadiumBorder(), // we'll fake underline via decoration below
NavBarM3EIndicatorStyle.underline =>
const StadiumBorder(), // we'll fake underline via decoration below
NavBarM3EIndicatorStyle.none => const StadiumBorder(),
},
backgroundColor: Colors.transparent, // outer Material supplies bg + shape
backgroundColor:
Colors.transparent, // outer Material supplies bg + shape
labelBehavior: switch (labelBehavior) {
NavBarM3ELabelBehavior.alwaysShow => NavigationDestinationLabelBehavior.alwaysShow,
NavBarM3ELabelBehavior.onlySelected => NavigationDestinationLabelBehavior.onlyShowSelected,
NavBarM3ELabelBehavior.alwaysHide => NavigationDestinationLabelBehavior.alwaysHide,
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),
selectedIcon: _selectedIcon(
context, true, d, metrics.iconSize, tokens, indicatorStyle),
label: d.label,
tooltip: d.semanticLabel,
);
@ -100,22 +110,29 @@ class NavigationBarM3E extends StatelessWidget {
final content = DefaultTextStyle.merge(
style: tokens.labelStyle().copyWith(
color: m3e.colors.onSurfaceVariant,
),
color: m3e.colors.onSurfaceVariant,
),
child: IconTheme.merge(
data: IconThemeData(size: metrics.iconSize, color: m3e.colors.onSurfaceVariant),
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);
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) {
Widget _icon(BuildContext context, bool selected, NavigationDestinationM3E d,
double iconSize) {
return SizedBox(
width: iconSize + 8, // give a little space for underline
height: iconSize + 8,
@ -135,7 +152,8 @@ class NavigationBarM3E extends StatelessWidget {
if (style != NavBarM3EIndicatorStyle.underline) return w;
final metrics = tokens.metrics(density);
final deco = tokens.underlineDecoration(tokens.indicatorColor(), metrics.indicatorThickness);
final deco = tokens.underlineDecoration(
tokens.indicatorColor(), metrics.indicatorThickness);
return DecoratedBox(
decoration: deco,
child: w,

View file

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