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:
Emily Pauli 2025-10-21 22:15:15 +02:00
commit 62ecb86b76
184 changed files with 9872 additions and 0 deletions

45
apps/gallery/.gitignore vendored Normal file
View file

@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

45
apps/gallery/.metadata Normal file
View file

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "d693b4b9dbac2acd4477aea4555ca6dcbea44ba2"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
- platform: android
create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
- platform: ios
create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
- platform: linux
create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
- platform: macos
create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
- platform: web
create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
- platform: windows
create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

16
apps/gallery/README.md Normal file
View file

@ -0,0 +1,16 @@
# m3e_gallery
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View file

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
# include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

560
apps/gallery/lib/main.dart Normal file
View file

@ -0,0 +1,560 @@
import 'package:flutter/material.dart';
import 'package:m3e_collection/m3e_collection.dart';
void main() => runApp(const GalleryApp());
class GalleryApp extends StatelessWidget {
const GalleryApp({super.key});
@override
Widget build(BuildContext context) {
final base = ThemeData(useMaterial3: true, colorSchemeSeed: Colors.purple);
return MaterialApp(
title: 'M3E Gallery',
theme: withM3ETheme(base),
home: const GalleryHome(),
debugShowCheckedModeBanner: false,
);
}
}
class GalleryHome extends StatefulWidget {
const GalleryHome({super.key});
@override
State<GalleryHome> createState() => _GalleryHomeState();
}
class _GalleryHomeState extends State<GalleryHome> {
// Navigation examples
int _navBarIndex = 0;
int _railIndex = 0;
// Slider examples
double _sliderValue = 0.4;
RangeValues _rangeValues = const RangeValues(0.25, 0.75);
// Progress examples
double _progressValue = 0.6;
void onPressed() {
// Placeholder function for button onPressed
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final m3e =
Theme.of(context).extension<M3ETheme>() ?? M3ETheme.defaults(cs);
return Scaffold(
appBar: AppBarM3E(
titleText: 'M3E Gallery',
actions: const [
Icon(Icons.search),
SizedBox(width: 8),
Icon(Icons.more_vert),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// Icon buttons
Text('IconButtonM3E', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
IconButtonM3E(
icon: Icon(Icons.favorite),
variant: IconButtonM3EVariant.filled,
onPressed: onPressed),
IconButtonM3E(
icon: Icon(Icons.favorite),
variant: IconButtonM3EVariant.tonal,
onPressed: onPressed),
IconButtonM3E(
icon: Icon(Icons.favorite),
variant: IconButtonM3EVariant.outlined,
onPressed: onPressed),
IconButtonM3E(
icon: Icon(Icons.favorite),
variant: IconButtonM3EVariant.standard,
onPressed: onPressed),
],
),
const SizedBox(height: 24),
// Split Button
Text('SplitButtonM3E', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
SplitButtonM3E<String>(
label: 'Primary',
onPressed: () {},
items: const [
SplitButtonM3EItem<String>(value: 'one', child: 'One'),
SplitButtonM3EItem<String>(value: 'two', child: 'Two'),
],
),
SplitButtonM3E<String>(
label: 'Tonal',
emphasis: SplitButtonM3EEmphasis.tonal,
onPressed: () {},
items: const [
SplitButtonM3EItem<String>(value: 'one', child: 'One'),
SplitButtonM3EItem<String>(value: 'two', child: 'Two'),
],
),
SplitButtonM3E<String>(
label: 'Outlined',
emphasis: SplitButtonM3EEmphasis.outlined,
onPressed: () {},
items: const [
SplitButtonM3EItem<String>(value: 'one', child: 'One'),
SplitButtonM3EItem<String>(value: 'two', child: 'Two'),
],
),
SplitButtonM3E<String>(
label: 'Text',
emphasis: SplitButtonM3EEmphasis.text,
onPressed: () {},
items: const [
SplitButtonM3EItem<String>(value: 'one', child: 'One'),
SplitButtonM3EItem<String>(value: 'two', child: 'Two'),
],
),
],
),
const SizedBox(height: 24),
// Buttons
Text('ButtonM3E', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
Wrap(spacing: 12, runSpacing: 12, children: [
Wrap(
spacing: 12,
runSpacing: 12,
children: [
ButtonM3E(
labelText: 'Filled',
variant: ButtonM3EVariant.filled,
onPressed: onPressed),
ButtonM3E(
labelText: 'Tonal',
variant: ButtonM3EVariant.tonal,
onPressed: onPressed),
ButtonM3E(
labelText: 'Outlined',
variant: ButtonM3EVariant.outlined,
onPressed: onPressed),
ButtonM3E(
labelText: 'Text',
variant: ButtonM3EVariant.text,
onPressed: onPressed),
ButtonM3E(
labelText: 'Elevated',
variant: ButtonM3EVariant.elevated,
onPressed: onPressed),
],
),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
ButtonM3E(
labelText: 'Filled',
variant: ButtonM3EVariant.filled,
shapeFamily: ButtonM3EShapeFamily.square,
onPressed: onPressed),
ButtonM3E(
labelText: 'Tonal',
variant: ButtonM3EVariant.tonal,
shapeFamily: ButtonM3EShapeFamily.square,
onPressed: onPressed),
ButtonM3E(
labelText: 'Outlined',
variant: ButtonM3EVariant.outlined,
shapeFamily: ButtonM3EShapeFamily.square,
onPressed: onPressed),
ButtonM3E(
labelText: 'Text',
variant: ButtonM3EVariant.text,
shapeFamily: ButtonM3EShapeFamily.square,
onPressed: onPressed),
ButtonM3E(
labelText: 'Elevated',
variant: ButtonM3EVariant.elevated,
shapeFamily: ButtonM3EShapeFamily.square,
onPressed: onPressed),
],
),
]),
const SizedBox(height: 24),
// Button groups
Text('ButtonGroupM3E', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
// Standard group, round, md
ButtonGroupM3E(
type: ButtonGroupM3EType.standard,
shape: ButtonGroupM3EShape.round,
size: ButtonGroupM3ESize.md,
children: const [
IconButtonM3E(icon: Icon(Icons.skip_previous)),
IconButtonM3E(icon: Icon(Icons.play_arrow)),
IconButtonM3E(icon: Icon(Icons.skip_next)),
],
),
const SizedBox(height: 12),
// Connected group with divider and clipping for outer corners
ButtonGroupM3E(
type: ButtonGroupM3EType.connected,
shape: ButtonGroupM3EShape.round,
size: ButtonGroupM3ESize.lg,
showDividers: true,
clipBehavior: Clip.hardEdge,
equalizeWidths: true,
semanticLabel: 'Actions',
children: [
SplitButtonM3E<String>(
label: 'Primary',
onPressed: () {},
items: const [
SplitButtonM3EItem<String>(value: 'one', child: 'One'),
SplitButtonM3EItem<String>(value: 'two', child: 'Two'),
],
),
SplitButtonM3E<String>(
label: 'Tonal',
emphasis: SplitButtonM3EEmphasis.tonal,
onPressed: () {},
items: const [
SplitButtonM3EItem<String>(value: 'one', child: 'One'),
SplitButtonM3EItem<String>(value: 'two', child: 'Two'),
],
),
],
),
const SizedBox(height: 12),
// Square, compact, vertical wrap
ButtonGroupM3E(
type: ButtonGroupM3EType.standard,
shape: ButtonGroupM3EShape.square,
size: ButtonGroupM3ESize.sm,
density: ButtonGroupM3EDensity.compact,
direction: Axis.vertical,
children: const [
IconButtonM3E(icon: Icon(Icons.view_agenda_outlined)),
IconButtonM3E(icon: Icon(Icons.table_rows_outlined)),
IconButtonM3E(icon: Icon(Icons.grid_view_outlined)),
],
),
const SizedBox(height: 24),
// Toolbar
Text('ToolbarM3E', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
ToolbarM3E(
titleText: 'Toolbar Title',
subtitleText: 'Subtitle',
actions: [
ToolbarActionM3E(icon: Icons.search, onPressed: () {}),
ToolbarActionM3E(icon: Icons.share, onPressed: () {}),
ToolbarActionM3E(
icon: Icons.delete,
onPressed: () {},
isDestructive: true,
label: 'Delete'),
ToolbarActionM3E(
icon: Icons.settings, onPressed: () {}, label: 'Settings'),
],
maxInlineActions: 2,
),
const SizedBox(height: 24),
// FABs
Text('FabM3E', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: const [
FabM3E(icon: Icon(Icons.add)),
FabM3E(icon: Icon(Icons.edit), kind: FabM3EKind.secondary),
FabM3E(icon: Icon(Icons.share), kind: FabM3EKind.tertiary),
FabM3E(icon: Icon(Icons.more_horiz), kind: FabM3EKind.surface),
FabM3E(icon: Icon(Icons.add), size: FabM3ESize.small),
FabM3E(icon: Icon(Icons.add), size: FabM3ESize.large),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: const [
ExtendedFabM3E(label: Text('Create'), icon: Icon(Icons.add)),
ExtendedFabM3E(
label: Text('Edit'),
icon: Icon(Icons.edit),
kind: FabM3EKind.secondary),
ExtendedFabM3E(
label: Text('Share'),
icon: Icon(Icons.share),
kind: FabM3EKind.tertiary),
],
),
const SizedBox(height: 12),
SizedBox(
height: 180,
child: Stack(
children: [
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: m3e.shapes.round.lg,
),
),
),
Align(
alignment: Alignment.bottomRight,
child: FabMenuM3E(
primaryFab: const FabM3E(icon: Icon(Icons.menu)),
items: [
FabMenuItem(
icon: const Icon(Icons.image),
label: const Text('Image'),
onPressed: () {}),
FabMenuItem(
icon: const Icon(Icons.camera_alt),
label: const Text('Camera'),
onPressed: () {}),
FabMenuItem(
icon: const Icon(Icons.file_upload),
label: const Text('Upload'),
onPressed: () {}),
],
),
),
],
),
),
const SizedBox(height: 24),
// Loading indicators
Text('LoadingIndicatorM3E',
style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
Wrap(
spacing: 16,
runSpacing: 16,
crossAxisAlignment: WrapCrossAlignment.center,
children: const [
LoadingIndicatorM3E(
variant: LoadingIndicatorM3EVariant.defaultStyle),
LoadingIndicatorM3E(
variant: LoadingIndicatorM3EVariant.contained),
],
),
const SizedBox(height: 24),
// Progress indicators
Text('ProgressIndicatorM3E',
style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
Wrap(
spacing: 16,
runSpacing: 16,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
const CircularProgressM3E(),
const CircularProgressM3E(size: ProgressM3ESize.small),
CircularProgressM3E(
size: ProgressM3ESize.large,
value: _progressValue,
showCenterLabel: true),
const LinearProgressM3E(minWidth: 200),
LinearProgressM3E(minWidth: 200, value: _progressValue),
const LinearProgressM3E(
minWidth: 200,
variant: LinearProgressM3EVariant.indeterminate),
const LinearProgressM3E(
minWidth: 200, variant: LinearProgressM3EVariant.query),
const LinearProgressM3E(
minWidth: 200,
variant: LinearProgressM3EVariant.buffer,
bufferValue: 0.8,
value: 0.4),
],
),
const SizedBox(height: 24),
// Sliders
Text('SliderM3E', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
SliderM3E(
value: _sliderValue,
onChanged: (v) => setState(() => _sliderValue = v),
min: 0,
max: 100,
label: '${_sliderValue.toStringAsFixed(0)}',
startIcon: const Icon(Icons.volume_mute),
endIcon: const Icon(Icons.volume_up),
),
const SizedBox(height: 8),
RangeSliderM3E(
values: _rangeValues,
onChanged: (v) => setState(() => _rangeValues = v),
min: 0,
max: 100,
labels: RangeLabels(
_rangeValues.start.toStringAsFixed(0),
_rangeValues.end.toStringAsFixed(0),
),
),
const SizedBox(height: 24),
// Navigation Bar
Text('NavigationBarM3E',
style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
NavigationBarM3E(
selectedIndex: _navBarIndex,
onDestinationSelected: (i) => setState(() => _navBarIndex = i),
indicatorStyle: NavBarM3EIndicatorStyle.pill,
destinations: const [
NavigationDestinationM3E(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: 'Home'),
NavigationDestinationM3E(
icon: Icon(Icons.search_outlined),
selectedIcon: Icon(Icons.search),
label: 'Search',
badgeDot: true),
NavigationDestinationM3E(
icon: Icon(Icons.favorite_outline),
selectedIcon: Icon(Icons.favorite),
label: 'Favorites',
badgeCount: 3),
NavigationDestinationM3E(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: 'Profile'),
],
),
const SizedBox(height: 24),
// Navigation Rail
Text('NavigationRailM3E',
style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: m3e.shapes.round.lg,
),
height: 180,
child: Row(
children: [
NavigationRailM3E(
selectedIndex: _railIndex,
onDestinationSelected: (i) => setState(() => _railIndex = i),
indicatorStyle: RailIndicatorStyle.pill,
destinations: const [
RailDestinationM3E(
icon: Icon(Icons.dashboard_outlined),
selectedIcon: Icon(Icons.dashboard),
label: 'Dashboard'),
RailDestinationM3E(
icon: Icon(Icons.analytics_outlined),
selectedIcon: Icon(Icons.analytics),
label: 'Reports'),
RailDestinationM3E(
icon: Icon(Icons.settings_outlined),
selectedIcon: Icon(Icons.settings),
label: 'Settings'),
],
),
const VerticalDivider(width: 1),
Expanded(
child: Center(
child: Text('Selected: $_railIndex'),
),
),
],
),
),
const SizedBox(height: 24),
// Sliver App Bar demo route
Text('App bars', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 12),
Wrap(
spacing: 12,
children: [
const ButtonM3E(labelText: 'Open SliverAppBarM3E Demo'),
// Use GestureDetector to navigate when tapping the button label area
]
.map((w) => GestureDetector(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const _SliverAppBarDemoPage()),
),
child: w,
))
.toList(),
),
const SizedBox(height: 48),
// Theming surface example remains
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: m3e.colors.surfaceStrong,
borderRadius: m3e.shapes.square.lg,
),
child: Text('Surface strong example',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(color: cs.onSurface)),
),
],
),
);
}
}
class _SliverAppBarDemoPage extends StatelessWidget {
const _SliverAppBarDemoPage();
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
const SliverAppBarM3E(
titleText: 'SliverAppBarM3E',
pinned: true,
floating: false,
variant: AppBarM3EVariant.large,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text('Item #$index'),
),
childCount: 30,
),
),
],
),
);
}
}

16
apps/gallery/pubspec.yaml Normal file
View file

@ -0,0 +1,16 @@
name: m3e_gallery
description: Gallery app for Material 3 Expressive packages.
publish_to: none
environment:
sdk: ">=3.5.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
m3e_collection:
path: ../../packages/m3e_collection
material_color_utilities: ^0.11.0
flutter:
uses-material-design: true

View file

@ -0,0 +1,30 @@
# melos_managed_dependency_overrides: icon_button_m3e,m3e_collection,m3e_design,split_button_m3e,app_bar_m3e,button_group_m3e,button_m3e,fab_m3e,loading_indicator_m3e,navigation_bar_m3e,navigation_rail_m3e,progress_indicator_m3e,slider_m3e,toolbar_m3e
dependency_overrides:
app_bar_m3e:
path: ..\\..\\packages\\app_bar_m3e
button_group_m3e:
path: ..\\..\\packages\\button_group_m3e
button_m3e:
path: ..\\..\\packages\\button_m3e
fab_m3e:
path: ..\\..\\packages\\fab_m3e
icon_button_m3e:
path: ..\\..\\packages\\icon_button_m3e
loading_indicator_m3e:
path: ..\\..\\packages\\loading_indicator_m3e
m3e_collection:
path: ..\\..\\packages\\m3e_collection
m3e_design:
path: ..\\..\\packages\\m3e_design
navigation_bar_m3e:
path: ..\\..\\packages\\navigation_bar_m3e
navigation_rail_m3e:
path: ..\\..\\packages\\navigation_rail_m3e
progress_indicator_m3e:
path: ..\\..\\packages\\progress_indicator_m3e
slider_m3e:
path: ..\\..\\packages\\slider_m3e
split_button_m3e:
path: ..\\..\\packages\\split_button_m3e
toolbar_m3e:
path: ..\\..\\packages\\toolbar_m3e