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
5
packages/app_bar_m3e/lib/app_bar_m3e.dart
Normal file
5
packages/app_bar_m3e/lib/app_bar_m3e.dart
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
library app_bar_m3e;
|
||||
|
||||
export 'src/app_bar_m3e_enums.dart';
|
||||
export 'src/app_bar_m3e_widget.dart';
|
||||
export 'src/sliver_app_bar_m3e.dart';
|
||||
74
packages/app_bar_m3e/lib/src/_tokens_adapter.dart
Normal file
74
packages/app_bar_m3e/lib/src/_tokens_adapter.dart
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_design/m3e_design.dart';
|
||||
import 'app_bar_m3e_enums.dart';
|
||||
|
||||
@immutable
|
||||
class _AppBarMetrics {
|
||||
final double smallHeight;
|
||||
final double collapsedHeight;
|
||||
final double mediumExpanded;
|
||||
final double largeExpanded;
|
||||
final EdgeInsetsGeometry horizontalPadding;
|
||||
final double iconSize;
|
||||
final double elevation;
|
||||
const _AppBarMetrics({
|
||||
required this.smallHeight,
|
||||
required this.collapsedHeight,
|
||||
required this.mediumExpanded,
|
||||
required this.largeExpanded,
|
||||
required this.horizontalPadding,
|
||||
required this.iconSize,
|
||||
required this.elevation,
|
||||
});
|
||||
}
|
||||
|
||||
_AppBarMetrics metricsFor(BuildContext context, AppBarM3EDensity density) {
|
||||
final theme = Theme.of(context);
|
||||
final m3e = theme.extension<M3ETheme>() ?? M3ETheme.defaults(theme.colorScheme);
|
||||
final sp = m3e.spacing;
|
||||
|
||||
// Heights (approx per M3 specs; can be tuned via Theme extension in m3e_design if desired)
|
||||
double small = 64;
|
||||
double collapsed = 64;
|
||||
double medium = 112;
|
||||
double large = 152;
|
||||
|
||||
// Density tweaks
|
||||
if (density == AppBarM3EDensity.compact) {
|
||||
small -= 8;
|
||||
collapsed -= 8;
|
||||
medium -= 8;
|
||||
large -= 8;
|
||||
}
|
||||
|
||||
return _AppBarMetrics(
|
||||
smallHeight: small,
|
||||
collapsedHeight: collapsed,
|
||||
mediumExpanded: medium,
|
||||
largeExpanded: large,
|
||||
horizontalPadding: EdgeInsets.symmetric(horizontal: sp.md),
|
||||
iconSize: 24,
|
||||
elevation: 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
Color backgroundFor(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final m3e = theme.extension<M3ETheme>() ?? M3ETheme.defaults(theme.colorScheme);
|
||||
// Prefer container surfaces for bars
|
||||
return m3e.colors.surfaceContainerHigh;
|
||||
}
|
||||
|
||||
TextStyle titleStyleFor(BuildContext context, {bool collapsed = true}) {
|
||||
final theme = Theme.of(context);
|
||||
final m3e = theme.extension<M3ETheme>() ?? M3ETheme.defaults(theme.colorScheme);
|
||||
return collapsed ? m3e.type.titleLarge : m3e.type.headlineSmallEmphasized;
|
||||
}
|
||||
|
||||
ShapeBorder shapeFor(BuildContext context, AppBarM3EShapeFamily family) {
|
||||
final theme = Theme.of(context);
|
||||
final m3e = theme.extension<M3ETheme>() ?? M3ETheme.defaults(theme.colorScheme);
|
||||
final set = family == AppBarM3EShapeFamily.round ? m3e.shapes.round : m3e.shapes.square;
|
||||
// Use medium size radius for the bar container by default
|
||||
return RoundedRectangleBorder(borderRadius: set.md);
|
||||
}
|
||||
3
packages/app_bar_m3e/lib/src/app_bar_m3e_enums.dart
Normal file
3
packages/app_bar_m3e/lib/src/app_bar_m3e_enums.dart
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
enum AppBarM3EVariant { small, medium, large }
|
||||
enum AppBarM3EShapeFamily { round, square }
|
||||
enum AppBarM3EDensity { regular, compact }
|
||||
134
packages/app_bar_m3e/lib/src/app_bar_m3e_widget.dart
Normal file
134
packages/app_bar_m3e/lib/src/app_bar_m3e_widget.dart
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '_tokens_adapter.dart';
|
||||
import 'app_bar_m3e_enums.dart';
|
||||
|
||||
class AppBarM3E extends StatelessWidget implements PreferredSizeWidget {
|
||||
const AppBarM3E({
|
||||
super.key,
|
||||
this.leading,
|
||||
this.title,
|
||||
this.titleText,
|
||||
this.actions,
|
||||
this.centerTitle = false,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
this.elevation,
|
||||
this.shapeFamily = AppBarM3EShapeFamily.round,
|
||||
this.density = AppBarM3EDensity.regular,
|
||||
this.toolbarHeight,
|
||||
this.automaticallyImplyLeading = true,
|
||||
this.clipBehavior = Clip.none,
|
||||
this.semanticLabel,
|
||||
});
|
||||
|
||||
final Widget? leading;
|
||||
final Widget? title;
|
||||
final String? titleText;
|
||||
final List<Widget>? actions;
|
||||
final bool centerTitle;
|
||||
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
final double? elevation;
|
||||
final AppBarM3EShapeFamily shapeFamily;
|
||||
final AppBarM3EDensity density;
|
||||
final double? toolbarHeight;
|
||||
final bool automaticallyImplyLeading;
|
||||
final Clip clipBehavior;
|
||||
final String? semanticLabel;
|
||||
|
||||
@override
|
||||
Size get preferredSize {
|
||||
// Provide a reasonable non-null size; actual height applied in build.
|
||||
return Size.fromHeight(toolbarHeight ?? 64);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final metrics = metricsFor(context, density);
|
||||
final bg = backgroundColor ?? backgroundFor(context);
|
||||
final fg = foregroundColor ?? Theme.of(context).colorScheme.onSurface;
|
||||
final shape = shapeFor(context, shapeFamily);
|
||||
final height = toolbarHeight ?? metrics.smallHeight;
|
||||
final tStyle = titleStyleFor(context, collapsed: true);
|
||||
|
||||
final resolvedLeading = leading ?? (automaticallyImplyLeading
|
||||
? _maybeBackButton(context, fg)
|
||||
: null);
|
||||
|
||||
final resolvedTitle = title ??
|
||||
(titleText != null
|
||||
? Text(titleText!, style: tStyle, overflow: TextOverflow.ellipsis)
|
||||
: null);
|
||||
|
||||
final bar = Material(
|
||||
color: bg,
|
||||
elevation: elevation ?? metrics.elevation,
|
||||
shape: shape,
|
||||
clipBehavior: clipBehavior,
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
child: Padding(
|
||||
padding: metrics.horizontalPadding,
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(size: metrics.iconSize, color: fg),
|
||||
child: DefaultTextStyle(
|
||||
style: tStyle.copyWith(color: fg),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (resolvedLeading != null) resolvedLeading,
|
||||
if (resolvedLeading != null) const SizedBox(width: 8),
|
||||
if (resolvedTitle != null)
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: centerTitle ? Alignment.center : Alignment.centerLeft,
|
||||
child: resolvedTitle,
|
||||
),
|
||||
)
|
||||
else
|
||||
const Spacer(),
|
||||
if (actions != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
..._withSpacers(actions!),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (semanticLabel == null) return bar;
|
||||
return Semantics(
|
||||
container: true,
|
||||
label: semanticLabel,
|
||||
child: bar,
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _withSpacers(List<Widget> items) {
|
||||
final out = <Widget>[];
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
out.add(items[i]);
|
||||
if (i < items.length - 1) out.add(const SizedBox(width: 4));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Widget? _maybeBackButton(BuildContext context, Color fg) {
|
||||
final canPop = Navigator.maybeOf(context)?.canPop() ?? false;
|
||||
if (!canPop) return null;
|
||||
return IconButton(
|
||||
icon: const BackButtonIcon(),
|
||||
color: fg,
|
||||
onPressed: () => Navigator.maybeOf(context)?.maybePop(),
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
);
|
||||
}
|
||||
}
|
||||
145
packages/app_bar_m3e/lib/src/sliver_app_bar_m3e.dart
Normal file
145
packages/app_bar_m3e/lib/src/sliver_app_bar_m3e.dart
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart' show RenderObject, RenderProxySliver;
|
||||
import 'package:flutter/semantics.dart' show SemanticsConfiguration;
|
||||
|
||||
import '_tokens_adapter.dart';
|
||||
import 'app_bar_m3e_enums.dart';
|
||||
|
||||
class SliverAppBarM3E extends StatelessWidget {
|
||||
const SliverAppBarM3E({
|
||||
super.key,
|
||||
this.leading,
|
||||
this.title,
|
||||
this.titleText,
|
||||
this.actions,
|
||||
this.centerTitle = false,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
this.pinned = true,
|
||||
this.floating = false,
|
||||
this.snap = false,
|
||||
this.shapeFamily = AppBarM3EShapeFamily.round,
|
||||
this.density = AppBarM3EDensity.regular,
|
||||
this.variant = AppBarM3EVariant.medium,
|
||||
this.semanticLabel,
|
||||
});
|
||||
|
||||
final Widget? leading;
|
||||
final Widget? title;
|
||||
final String? titleText;
|
||||
final List<Widget>? actions;
|
||||
final bool centerTitle;
|
||||
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
final bool pinned;
|
||||
final bool floating;
|
||||
final bool snap;
|
||||
final AppBarM3EShapeFamily shapeFamily;
|
||||
final AppBarM3EDensity density;
|
||||
final AppBarM3EVariant variant;
|
||||
final String? semanticLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final metrics = metricsFor(context, density);
|
||||
final bg = backgroundColor ?? backgroundFor(context);
|
||||
final fg = foregroundColor ?? Theme.of(context).colorScheme.onSurface;
|
||||
final shape = shapeFor(context, shapeFamily);
|
||||
|
||||
final collapsedStyle = titleStyleFor(context, collapsed: true);
|
||||
final expandedStyle = titleStyleFor(context, collapsed: false);
|
||||
|
||||
final collapsed = metrics.collapsedHeight;
|
||||
final expanded = switch (variant) {
|
||||
AppBarM3EVariant.medium => metrics.mediumExpanded,
|
||||
AppBarM3EVariant.large => metrics.largeExpanded,
|
||||
AppBarM3EVariant.small => metrics.smallHeight,
|
||||
};
|
||||
|
||||
final resolvedTitleWidget = title ??
|
||||
(titleText != null
|
||||
? Text(titleText!,
|
||||
style: collapsedStyle, overflow: TextOverflow.ellipsis)
|
||||
: null);
|
||||
|
||||
final bar = SliverAppBar(
|
||||
pinned: pinned,
|
||||
floating: floating,
|
||||
snap: snap && floating,
|
||||
backgroundColor: bg,
|
||||
foregroundColor: fg,
|
||||
collapsedHeight: collapsed,
|
||||
expandedHeight: expanded,
|
||||
centerTitle: centerTitle,
|
||||
leading: leading,
|
||||
title: resolvedTitleWidget,
|
||||
actions: actions,
|
||||
shape: shape,
|
||||
flexibleSpace: _buildFlexibleSpace(context, expandedStyle),
|
||||
);
|
||||
|
||||
if (semanticLabel == null) return bar;
|
||||
return SliverSemantic(
|
||||
label: semanticLabel!,
|
||||
child: bar,
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildFlexibleSpace(BuildContext context, TextStyle expandedStyle) {
|
||||
switch (variant) {
|
||||
case AppBarM3EVariant.small:
|
||||
return null;
|
||||
case AppBarM3EVariant.medium:
|
||||
case AppBarM3EVariant.large:
|
||||
final t = title ??
|
||||
(titleText != null ? Text(titleText!, style: expandedStyle) : null);
|
||||
if (t == null) return null;
|
||||
return FlexibleSpaceBar(
|
||||
titlePadding:
|
||||
const EdgeInsetsDirectional.only(start: 16, bottom: 16, end: 16),
|
||||
title: DefaultTextStyle(
|
||||
style: expandedStyle.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
child: t,
|
||||
),
|
||||
collapseMode: CollapseMode.pin,
|
||||
expandedTitleScale:
|
||||
1.0, // Typography already larger; avoid scale morph
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper to wrap a sliver with semantics label.
|
||||
class SliverSemantic extends SingleChildRenderObjectWidget {
|
||||
const SliverSemantic({super.key, required this.label, required Widget child})
|
||||
: super(child: child);
|
||||
final String label;
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) =>
|
||||
_SliverSemanticRender(label);
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context, covariant _SliverSemanticRender renderObject) {
|
||||
renderObject.label = label;
|
||||
}
|
||||
}
|
||||
|
||||
class _SliverSemanticRender extends RenderProxySliver {
|
||||
_SliverSemanticRender(this._label);
|
||||
String _label;
|
||||
set label(String v) {
|
||||
if (v == _label) return;
|
||||
_label = v;
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
@override
|
||||
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||
super.describeSemanticsConfiguration(config);
|
||||
config.label = _label;
|
||||
config.isSemanticBoundary = true;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue