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

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) ...
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,65 @@
# progress_indicators_m3e
Material 3 **Expressive** progress indicators for Flutter:
- `LinearProgressM3E` — determinate, indeterminate, **buffer**, **query**, with **flat** or **wavy** shape
- `CircularProgressM3E` — determinate & indeterminate, **flat** or **wavy** stroke (animated for indeterminate)
- `ProgressWithLabelM3E` — compose a linear bar with inline/top/bottom/center labels
All widgets read **M3E tokens** (`m3e_design`) for color, sizing, and typography.
## Defaults (from the spec illustrations)
- Linear: default thickness 4dp; configurable via `size` or `strokeHeight`
- Linear (wavy): `wavelength=40dp`, `amplitude≈height/3`, **4dp** left/right inset
- Circular: small/medium/large diameters ≈ 24/32/48 with stroke ≈ 3/4/6
- Circular (wavy): default **10 waves** around the circle, amplitude ≈ 35% of stroke
## Quick start
```dart
import 'package:progress_indicators_m3e/progress_indicators_m3e.dart';
// Linear (wavy, determinate)
LinearProgressM3E(
value: 0.62,
shape: LinearBarShapeM3E.wavy,
);
// Circular (wavy, indeterminate)
const CircularProgressM3E(
shape: CircularBarShapeM3E.wavy,
);
// Linear (buffer) flat
LinearProgressM3E(
variant: LinearProgressM3EVariant.buffer,
value: 0.3,
bufferValue: 0.6,
);
// Circular (flat) with center label
CircularProgressM3E(
value: 0.5,
showCenterLabel: true,
);
```
## Monorepo layout
```
packages/
m3e_design/
progress_indicators_m3e/
```
`pubspec.yaml` references `../m3e_design`.
## Accessibility
- Provide `semanticLabel` and (for determinate) the widgets expose a numeric **value** for screen readers.
- Indeterminate wavy animations use modest motion; gate the speed with a future `m3e_design.motion` flag if you support "reduce motion".
## License
MIT

View file

@ -0,0 +1,7 @@
library progress_indicators_m3e;
export 'src/enums.dart';
export 'src/tokens_adapter.dart' show ProgressTokensAdapter;
export 'src/linear_progress_m3e.dart';
export 'src/circular_progress_m3e.dart';
export 'src/progress_with_label_m3e.dart';

View file

@ -0,0 +1,286 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'enums.dart';
import 'tokens_adapter.dart';
class CircularProgressM3E extends StatefulWidget {
const CircularProgressM3E({
super.key,
this.value,
this.size = ProgressM3ESize.medium,
this.emphasis = ProgressM3EEmphasis.primary,
this.density = ProgressM3EDensity.regular,
this.backgroundColor,
this.strokeWidth,
this.semanticLabel,
this.showCenterLabel = false,
this.centerLabelBuilder,
this.shape = CircularBarShapeM3E.wavy,
this.waveCount,
this.waveAmplitude,
this.rotateClockwise = true,
});
/// Determinate value (0..1). If null, renders indeterminate.
final double? value;
final ProgressM3ESize size;
final ProgressM3EEmphasis emphasis;
final ProgressM3EDensity density;
final Color? backgroundColor;
final double? strokeWidth;
/// Optional semantics label.
final String? semanticLabel;
/// Show a label centered inside (e.g., percentage).
final bool showCenterLabel;
/// Builder for custom center label; if null and showCenterLabel==true, shows percentage text.
final Widget Function(BuildContext context, double? value)?
centerLabelBuilder;
/// Expressive shape
final CircularBarShapeM3E shape;
final int? waveCount;
final double? waveAmplitude;
final bool rotateClockwise;
@override
State<CircularProgressM3E> createState() => _CircularProgressM3EState();
}
class _CircularProgressM3EState extends State<CircularProgressM3E>
with SingleTickerProviderStateMixin {
late final AnimationController _anim = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1400),
)..repeat();
@override
void dispose() {
_anim.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final tokens = ProgressTokensAdapter(context);
final m = tokens.metrics(widget.density);
final color = tokens.color(widget.emphasis);
final bg = widget.backgroundColor ?? tokens.trackColor();
final (diameter, stroke) = switch (widget.size) {
ProgressM3ESize.small => (
m.circularSmall,
widget.strokeWidth ?? m.strokeSmall
),
ProgressM3ESize.medium => (
m.circularMedium,
widget.strokeWidth ?? m.strokeMedium
),
ProgressM3ESize.large => (
m.circularLarge,
widget.strokeWidth ?? m.strokeLarge
),
};
final indicator = SizedBox(
width: diameter,
height: diameter,
child: Stack(
alignment: Alignment.center,
children: [
// Track ring
CustomPaint(
size: Size.square(diameter),
painter: _RingPainter(color: bg, stroke: stroke),
),
// Progress
if (widget.shape == CircularBarShapeM3E.flat) ...[
CustomPaint(
size: Size.square(diameter),
painter: _ArcPainter(
color: color,
stroke: stroke,
value: widget.value,
clockwise: widget.rotateClockwise,
),
),
] else ...[
AnimatedBuilder(
animation: _anim,
builder: (context, _) => CustomPaint(
size: Size.square(diameter),
painter: _WavyArcPainter(
color: color,
stroke: stroke,
value: widget.value,
waves: widget.waveCount ?? m.circularWavesPerCircle,
amplitude: (widget.waveAmplitude ??
(m.circularWaveAmplitudeFactor * stroke))
.clamp(0, stroke / 2),
phase:
(widget.value == null ? 2 * math.pi * _anim.value : 0) *
(widget.rotateClockwise ? 1 : -1),
clockwise: widget.rotateClockwise,
),
),
),
],
if (widget.showCenterLabel)
DefaultTextStyle(
style: tokens.labelStyle().copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
child: widget.centerLabelBuilder?.call(context, widget.value) ??
Text(widget.value != null
? '${(widget.value! * 100).toStringAsFixed(0)}%'
: ''),
),
],
),
);
if (widget.semanticLabel == null) return indicator;
return Semantics(
label: widget.semanticLabel,
value: widget.value != null
? '${(widget.value! * 100).toStringAsFixed(0)}%'
: null,
child: indicator,
);
}
}
class _RingPainter extends CustomPainter {
_RingPainter({required this.color, required this.stroke});
final Color color;
final double stroke;
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = stroke
..strokeCap = StrokeCap.round;
final rect = Offset.zero & size;
final center = rect.center;
final radius = (size.shortestSide - stroke) / 2;
canvas.drawCircle(center, radius, paint);
}
@override
bool shouldRepaint(covariant _RingPainter old) =>
old.color != color || old.stroke != stroke;
}
class _ArcPainter extends CustomPainter {
_ArcPainter({
required this.color,
required this.stroke,
required this.value,
required this.clockwise,
});
final Color color;
final double stroke;
final double? value;
final bool clockwise;
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = stroke
..strokeCap = StrokeCap.round;
final rect = Offset.zero & size;
final center = rect.center;
final radius = (size.shortestSide - stroke) / 2;
final start = -math.pi / 2;
final sweep = (value ?? 0.25) * 2 * math.pi * (clockwise ? 1 : -1);
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), start,
sweep, false, paint);
if (value == null) {
// indeterminate - draw a moving arc; this painter is used only for determinate (flat)
}
}
@override
bool shouldRepaint(covariant _ArcPainter old) =>
old.color != color ||
old.stroke != stroke ||
old.value != value ||
old.clockwise != clockwise;
}
class _WavyArcPainter extends CustomPainter {
_WavyArcPainter({
required this.color,
required this.stroke,
required this.value,
required this.waves,
required this.amplitude,
required this.phase,
required this.clockwise,
});
final Color color;
final double stroke;
final double? value;
final int waves;
final double amplitude;
final double phase;
final bool clockwise;
@override
void paint(Canvas canvas, Size size) {
final rect = Offset.zero & size;
final center = rect.center;
final baseRadius = (size.shortestSide - stroke) / 2;
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = stroke
..strokeCap = StrokeCap.round;
final totalAngle = (value ?? 1.0) * 2 * math.pi * (clockwise ? 1 : -1);
final start = -math.pi / 2;
final path = Path();
final steps = (200 * (value ?? 1.0)).clamp(40, 300).toInt(); // resolution
for (int i = 0; i <= steps; i++) {
final t = i / steps;
final theta = start + totalAngle * t;
final wave = math.sin((t * waves * 2 * math.pi) + phase);
final r = baseRadius + amplitude * wave;
final p = Offset(
center.dx + r * math.cos(theta), center.dy + r * math.sin(theta));
if (i == 0) {
path.moveTo(p.dx, p.dy);
} else {
path.lineTo(p.dx, p.dy);
}
}
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant _WavyArcPainter old) =>
old.color != color ||
old.stroke != stroke ||
old.value != value ||
old.waves != waves ||
old.amplitude != amplitude ||
old.phase != phase ||
old.clockwise != clockwise;
}

View file

@ -0,0 +1,8 @@
enum ProgressM3ESize { small, medium, large }
enum ProgressM3EEmphasis { primary, secondary, surface }
enum ProgressM3EDensity { regular, compact }
enum LinearProgressM3EVariant { determinate, indeterminate, buffer, query }
enum ProgressLabelPosition { none, leading, trailing, top, bottom, center }
enum LinearBarShapeM3E { flat, wavy }
enum CircularBarShapeM3E { flat, wavy }

View file

@ -0,0 +1,378 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'enums.dart';
import 'tokens_adapter.dart';
class LinearProgressM3E extends StatefulWidget {
const LinearProgressM3E({
super.key,
this.value,
this.bufferValue,
this.variant = LinearProgressM3EVariant.determinate,
this.size = ProgressM3ESize.medium,
this.emphasis = ProgressM3EEmphasis.primary,
this.density = ProgressM3EDensity.regular,
this.backgroundColor,
this.progressColor,
this.bufferColor,
this.semanticLabel,
this.minWidth = double.infinity,
this.strokeHeight,
this.borderRadius,
this.shape = LinearBarShapeM3E.wavy,
this.wavelength,
this.amplitude,
this.leftRightInset,
});
final double? value;
final double? bufferValue;
final LinearProgressM3EVariant variant;
final ProgressM3ESize size;
final ProgressM3EEmphasis emphasis;
final ProgressM3EDensity density;
final Color? backgroundColor;
final Color? progressColor;
final Color? bufferColor;
final String? semanticLabel;
final double minWidth;
final double? strokeHeight;
final BorderRadius? borderRadius;
final LinearBarShapeM3E shape;
final double? wavelength;
final double? amplitude;
final double? leftRightInset;
@override
State<LinearProgressM3E> createState() => _LinearProgressM3EState();
}
class _LinearProgressM3EState extends State<LinearProgressM3E>
with SingleTickerProviderStateMixin {
late final AnimationController _anim = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
)..repeat();
@override
void dispose() {
_anim.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final tokens = ProgressTokensAdapter(context);
final m = tokens.metrics(widget.density);
final height = switch (widget.size) {
ProgressM3ESize.small => widget.strokeHeight ?? m.linearThicknessSmall,
ProgressM3ESize.medium => widget.strokeHeight ?? m.linearThicknessMedium,
ProgressM3ESize.large => widget.strokeHeight ?? m.linearThicknessLarge,
};
final track = widget.backgroundColor ?? tokens.trackColor();
final progress = widget.progressColor ?? tokens.color(widget.emphasis);
final buffer = widget.bufferColor ?? tokens.bufferColor(progress);
final borderRadius =
widget.borderRadius ?? BorderRadius.circular(height / 2);
final inset = widget.leftRightInset ?? m.horizontalInset;
final content = Padding(
padding: EdgeInsets.symmetric(horizontal: inset),
child: _buildBar(
context, height, borderRadius, track, progress, buffer, tokens),
);
final bar = ClipRRect(
borderRadius: borderRadius,
child: SizedBox(
height: height,
width: widget.minWidth == double.infinity ? null : widget.minWidth,
child: content,
),
);
if (widget.semanticLabel == null) return bar;
return Semantics(
label: widget.semanticLabel,
value: (widget.variant == LinearProgressM3EVariant.determinate &&
widget.value != null)
? '${(widget.value!.clamp(0.0, 1.0) * 100).toStringAsFixed(0)}%'
: null,
child: bar,
);
}
Widget _buildBar(
BuildContext context,
double height,
BorderRadius borderRadius,
Color track,
Color progress,
Color buffer,
ProgressTokensAdapter tokens,
) {
final variant = widget.variant;
final shape = widget.shape;
if (shape == LinearBarShapeM3E.flat) {
// Use standard LinearProgressIndicator behaviors.
if (variant == LinearProgressM3EVariant.indeterminate ||
(variant == LinearProgressM3EVariant.determinate &&
widget.value == null)) {
return LinearProgressIndicator(
color: progress,
backgroundColor: track,
minHeight: height,
);
} else if (variant == LinearProgressM3EVariant.query) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()..scale(-1.0, 1.0, 1.0),
child: LinearProgressIndicator(
color: progress,
backgroundColor: track,
minHeight: height,
),
);
} else if (variant == LinearProgressM3EVariant.buffer) {
return _BufferBar(
height: height,
track: track,
buffer: buffer,
progress: progress,
value: widget.value ?? 0.0,
bufferValue: widget.bufferValue ?? 0.0,
);
} else {
return LinearProgressIndicator(
value: (widget.value ?? 0.0).clamp(0.0, 1.0),
color: progress,
backgroundColor: track,
minHeight: height,
);
}
}
final wavelength =
widget.wavelength ?? tokens.metrics(widget.density).wavyWavelength;
final amplitude = widget.amplitude ??
tokens.metrics(widget.density).wavyAmplitudeFactor * height;
if (variant == LinearProgressM3EVariant.determinate &&
widget.value != null) {
return _WavyBar(
value: widget.value!.clamp(0.0, 1.0),
height: height,
wavelength: wavelength,
amplitude: amplitude.clamp(0.0, height / 2),
track: track,
fill: progress,
);
}
// Indeterminate / query / missing value animate phase
return AnimatedBuilder(
animation: _anim,
builder: (context, _) {
final phase = 2 * math.pi * _anim.value;
final reverse = widget.variant == LinearProgressM3EVariant.query;
return _WavyIndeterminateBar(
height: height,
wavelength: wavelength,
amplitude: amplitude.clamp(0.0, height / 2),
track: track,
fill: progress,
phase: reverse ? -phase : phase,
);
},
);
}
}
class _BufferBar extends StatelessWidget {
const _BufferBar({
required this.height,
required this.track,
required this.buffer,
required this.progress,
required this.value,
required this.bufferValue,
});
final double height;
final Color track;
final Color buffer;
final Color progress;
final double value;
final double bufferValue;
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final w = constraints.maxWidth;
final pv = (w.isFinite ? w : 0) * value.clamp(0.0, 1.0);
final bv = (w.isFinite ? w : 0) * bufferValue.clamp(0.0, 1.0);
Widget seg(double width, Color color) => Align(
alignment: Alignment.centerLeft,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutCubic,
width: width,
height: height,
color: color,
),
);
return Stack(
fit: StackFit.passthrough,
children: [
ColoredBox(color: track),
seg(bv, buffer),
seg(pv, progress),
],
);
});
}
}
class _WavyBar extends StatelessWidget {
const _WavyBar({
required this.value,
required this.height,
required this.wavelength,
required this.amplitude,
required this.track,
required this.fill,
});
final double value;
final double height;
final double wavelength;
final double amplitude;
final Color track;
final Color fill;
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _WavyPainter(
value: value,
height: height,
wavelength: wavelength,
amplitude: amplitude,
track: track,
fill: fill,
phase: 0,
indeterminate: false,
),
);
}
}
class _WavyIndeterminateBar extends StatelessWidget {
const _WavyIndeterminateBar({
required this.height,
required this.wavelength,
required this.amplitude,
required this.track,
required this.fill,
required this.phase,
});
final double height;
final double wavelength;
final double amplitude;
final Color track;
final Color fill;
final double phase;
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _WavyPainter(
value: 0.6,
height: height,
wavelength: wavelength,
amplitude: amplitude,
track: track,
fill: fill,
phase: phase,
indeterminate: true,
),
);
}
}
class _WavyPainter extends CustomPainter {
_WavyPainter({
required this.value,
required this.height,
required this.wavelength,
required this.amplitude,
required this.track,
required this.fill,
required this.phase,
required this.indeterminate,
});
final double value;
final double height;
final double wavelength;
final double amplitude;
final Color track;
final Color fill;
final double phase;
final bool indeterminate;
@override
void paint(Canvas canvas, Size size) {
final paintTrack = Paint()..color = track;
final paintFill = Paint()..color = fill;
final r = RRect.fromRectAndRadius(
Offset.zero & Size(size.width, height), Radius.circular(height / 2));
canvas.drawRRect(r, paintTrack);
final w = size.width;
final progressW = indeterminate ? w : (w * value.clamp(0.0, 1.0));
final centerY = height / 2;
final path = Path()..moveTo(0, height);
path.lineTo(0, centerY);
final k = 2 * math.pi / wavelength;
final step = 2.0;
double x = 0;
while (x <= progressW) {
final y = centerY - amplitude * math.sin(k * x + phase);
path.lineTo(x, y);
x += step;
}
path.lineTo(progressW, height);
path.close();
canvas.save();
final clip = Path()..addRRect(r);
canvas.clipPath(clip);
canvas.drawPath(path, paintFill);
canvas.restore();
}
@override
bool shouldRepaint(covariant _WavyPainter old) {
return old.value != value ||
old.height != height ||
old.wavelength != wavelength ||
old.amplitude != amplitude ||
old.phase != phase ||
old.track != track ||
old.fill != fill ||
old.indeterminate != indeterminate;
}
}

View file

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'enums.dart';
import 'tokens_adapter.dart';
import 'linear_progress_m3e.dart';
class ProgressWithLabelM3E extends StatelessWidget {
const ProgressWithLabelM3E({
super.key,
required this.progress,
this.position = ProgressLabelPosition.trailing,
this.label,
this.spacing,
this.textStyle,
});
final LinearProgressM3E progress;
final ProgressLabelPosition position;
final Widget? label;
final double? spacing;
final TextStyle? textStyle;
@override
Widget build(BuildContext context) {
if (position == ProgressLabelPosition.none) return progress;
final tokens = ProgressTokensAdapter(context);
final style = textStyle ?? tokens.labelStyle().copyWith(
color: Theme.of(context).colorScheme.onSurface,
);
final gap = spacing ?? 8.0;
final value = progress.value;
final builtLabel = label ?? Text(
value != null ? '${(value * 100).toStringAsFixed(0)}%' : '',
style: style,
);
switch (position) {
case ProgressLabelPosition.leading:
case ProgressLabelPosition.trailing:
final children = <Widget>[
if (position == ProgressLabelPosition.leading) builtLabel,
if (position == ProgressLabelPosition.leading) SizedBox(width: gap),
Expanded(child: progress),
if (position == ProgressLabelPosition.trailing) SizedBox(width: gap),
if (position == ProgressLabelPosition.trailing) builtLabel,
];
return Row(children: children);
case ProgressLabelPosition.top:
case ProgressLabelPosition.bottom:
final children = <Widget>[
if (position == ProgressLabelPosition.top) builtLabel,
if (position == ProgressLabelPosition.top) SizedBox(height: gap),
progress,
if (position == ProgressLabelPosition.bottom) SizedBox(height: gap),
if (position == ProgressLabelPosition.bottom) builtLabel,
];
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: children,
);
case ProgressLabelPosition.center:
return Stack(
alignment: Alignment.center,
children: [
progress,
builtLabel,
],
);
case ProgressLabelPosition.none:
return progress;
}
}
}

View file

@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:m3e_design/m3e_design.dart';
import 'enums.dart';
@immutable
class _ProgressMetrics {
final double circularSmall;
final double circularMedium;
final double circularLarge;
final double strokeSmall;
final double strokeMedium;
final double strokeLarge;
final double linearThicknessSmall;
final double linearThicknessMedium;
final double linearThicknessLarge;
final double wavyWavelength; // dp (linear)
final double wavyAmplitudeFactor; // fraction of bar height (linear)
final double horizontalInset; // 4dp
final double circularWaveAmplitudeFactor; // fraction of stroke
final int circularWavesPerCircle; // rough default
const _ProgressMetrics({
required this.circularSmall,
required this.circularMedium,
required this.circularLarge,
required this.strokeSmall,
required this.strokeMedium,
required this.strokeLarge,
required this.linearThicknessSmall,
required this.linearThicknessMedium,
required this.linearThicknessLarge,
required this.wavyWavelength,
required this.wavyAmplitudeFactor,
required this.horizontalInset,
required this.circularWaveAmplitudeFactor,
required this.circularWavesPerCircle,
});
}
_ProgressMetrics _metricsFor(BuildContext context, ProgressM3EDensity density) {
double cS = 24, cM = 32, cL = 48;
double sS = 3, sM = 4, sL = 6;
double ltS = 3, ltM = 4, ltL = 6;
if (density == ProgressM3EDensity.compact) {
cS -= 2; cM -= 2; cL -= 4;
sS -= 0.5; sM -= 0.5; sL -= 1;
ltS -= 0.5; ltM -= 0.5; ltL -= 1;
}
return _ProgressMetrics(
circularSmall: cS,
circularMedium: cM,
circularLarge: cL,
strokeSmall: sS,
strokeMedium: sM,
strokeLarge: sL,
linearThicknessSmall: ltS,
linearThicknessMedium: ltM,
linearThicknessLarge: ltL,
wavyWavelength: 40, // per spec illustration
wavyAmplitudeFactor: 0.33, // amplitude 1/3 of height
horizontalInset: 4, // 4dp inset L/R
circularWaveAmplitudeFactor: 0.35, // ~1/3 of stroke
circularWavesPerCircle: 10, // a nice default
);
}
class ProgressTokensAdapter {
ProgressTokensAdapter(this.context);
final BuildContext context;
M3ETheme get _m3e {
final t = Theme.of(context);
return t.extension<M3ETheme>() ?? M3ETheme.defaults(t.colorScheme);
}
_ProgressMetrics metrics(ProgressM3EDensity density) => _metricsFor(context, density);
Color color(ProgressM3EEmphasis emphasis) {
switch (emphasis) {
case ProgressM3EEmphasis.primary:
return _m3e.colors.primary;
case ProgressM3EEmphasis.secondary:
return _m3e.colors.secondary;
case ProgressM3EEmphasis.surface:
return _m3e.colors.onSurface;
}
}
Color trackColor() => _m3e.colors.onSurface.withOpacity(0.12);
Color bufferColor(Color progress) => progress.withOpacity(0.24);
TextStyle labelStyle() => _m3e.type.bodySmall;
}

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.idea" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/example/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/example/.idea" />
<excludeFolder url="file://$MODULE_DIR$/example/.pub" />
<excludeFolder url="file://$MODULE_DIR$/example/build" />
<excludeFolder url="file://$MODULE_DIR$/example/android/.gradle" />
<excludeFolder url="file://$MODULE_DIR$/example/android/.idea" />
<excludeFolder url="file://$MODULE_DIR$/example/ios/Flutter" />
<excludeFolder url="file://$MODULE_DIR$/example/ios/Pods" />
<excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks" />
<excludeFolder url="file://$MODULE_DIR$/example/macos/Flutter" />
<excludeFolder url="file://$MODULE_DIR$/example/macos/Pods" />
<excludeFolder url="file://$MODULE_DIR$/example/macos/.symlinks" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Flutter Plugins" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

View file

@ -0,0 +1,18 @@
name: progress_indicator_m3e
description: Material 3 Expressive Progress Indicator for Flutter (linear + circular; flat + wavy) using M3E tokens.
version: 0.1.0
publish_to: none
environment:
sdk: ">=3.5.0 <4.0.0"
flutter: ">=3.22.0"
dependencies:
flutter:
sdk: flutter
m3e_design:
path: ../m3e_design
dev_dependencies:
flutter_test:
sdk: flutter

View file

@ -0,0 +1,4 @@
# melos_managed_dependency_overrides: m3e_design
dependency_overrides:
m3e_design:
path: ..\\m3e_design

View file

@ -0,0 +1,7 @@
import 'package:flutter_test/flutter_test.dart';
void main() {
test('placeholder', () {
expect(1 + 2, 3);
});
}