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/slider_m3e/lib/slider_m3e.dart
Normal file
7
packages/slider_m3e/lib/slider_m3e.dart
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
library slider_m3e;
|
||||
|
||||
export 'src/enums.dart';
|
||||
export 'src/slider_tokens_adapter.dart' show SliderTokensAdapter;
|
||||
export 'src/slider_theme_m3e.dart';
|
||||
export 'src/slider_m3e.dart';
|
||||
export 'src/range_slider_m3e.dart';
|
||||
4
packages/slider_m3e/lib/src/enums.dart
Normal file
4
packages/slider_m3e/lib/src/enums.dart
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
enum SliderM3ESize { small, medium, large }
|
||||
enum SliderM3EEmphasis { primary, secondary, surface }
|
||||
enum SliderM3EShapeFamily { round, square }
|
||||
enum SliderM3EDensity { regular, compact }
|
||||
71
packages/slider_m3e/lib/src/range_slider_m3e.dart
Normal file
71
packages/slider_m3e/lib/src/range_slider_m3e.dart
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'slider_theme_m3e.dart';
|
||||
import 'enums.dart';
|
||||
|
||||
class RangeSliderM3E extends StatelessWidget {
|
||||
const RangeSliderM3E({
|
||||
super.key,
|
||||
required this.values,
|
||||
required this.onChanged,
|
||||
this.onChangeStart,
|
||||
this.onChangeEnd,
|
||||
this.min = 0.0,
|
||||
this.max = 1.0,
|
||||
this.divisions,
|
||||
this.labels,
|
||||
this.semanticLabel,
|
||||
this.size = SliderM3ESize.medium,
|
||||
this.emphasis = SliderM3EEmphasis.primary,
|
||||
this.shapeFamily = SliderM3EShapeFamily.round,
|
||||
this.density = SliderM3EDensity.regular,
|
||||
this.showValueIndicator,
|
||||
});
|
||||
|
||||
final RangeValues values;
|
||||
final ValueChanged<RangeValues>? onChanged;
|
||||
final ValueChanged<RangeValues>? onChangeStart;
|
||||
final ValueChanged<RangeValues>? onChangeEnd;
|
||||
final double min;
|
||||
final double max;
|
||||
final int? divisions;
|
||||
final RangeLabels? labels;
|
||||
final String? semanticLabel;
|
||||
|
||||
final SliderM3ESize size;
|
||||
final SliderM3EEmphasis emphasis;
|
||||
final SliderM3EShapeFamily shapeFamily;
|
||||
final SliderM3EDensity density;
|
||||
final bool? showValueIndicator;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = sliderThemeM3E(
|
||||
context,
|
||||
size: size,
|
||||
emphasis: emphasis,
|
||||
shapeFamily: shapeFamily,
|
||||
density: density,
|
||||
showValueIndicator: showValueIndicator ?? false,
|
||||
);
|
||||
|
||||
return SliderTheme(
|
||||
data: theme,
|
||||
child: RangeSlider(
|
||||
values: RangeValues(
|
||||
values.start.clamp(min, max),
|
||||
values.end.clamp(min, max),
|
||||
),
|
||||
onChanged: onChanged,
|
||||
onChangeStart: onChangeStart,
|
||||
onChangeEnd: onChangeEnd,
|
||||
min: min,
|
||||
max: max,
|
||||
divisions: divisions,
|
||||
labels: labels,
|
||||
semanticFormatterCallback: semanticLabel != null
|
||||
? (v) => '$semanticLabel ${(100 * ((v - min) / (max - min))).toStringAsFixed(0)}%'
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
85
packages/slider_m3e/lib/src/slider_m3e.dart
Normal file
85
packages/slider_m3e/lib/src/slider_m3e.dart
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'slider_theme_m3e.dart';
|
||||
import 'enums.dart';
|
||||
|
||||
class SliderM3E extends StatelessWidget {
|
||||
const SliderM3E({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
this.onChangeStart,
|
||||
this.onChangeEnd,
|
||||
this.min = 0.0,
|
||||
this.max = 1.0,
|
||||
this.divisions,
|
||||
this.label,
|
||||
this.semanticLabel,
|
||||
this.size = SliderM3ESize.medium,
|
||||
this.emphasis = SliderM3EEmphasis.primary,
|
||||
this.shapeFamily = SliderM3EShapeFamily.round,
|
||||
this.density = SliderM3EDensity.regular,
|
||||
this.showValueIndicator,
|
||||
this.startIcon,
|
||||
this.endIcon,
|
||||
});
|
||||
|
||||
final double value;
|
||||
final ValueChanged<double>? onChanged;
|
||||
final ValueChanged<double>? onChangeStart;
|
||||
final ValueChanged<double>? onChangeEnd;
|
||||
final double min;
|
||||
final double max;
|
||||
final int? divisions;
|
||||
final String? label;
|
||||
final String? semanticLabel;
|
||||
|
||||
final SliderM3ESize size;
|
||||
final SliderM3EEmphasis emphasis;
|
||||
final SliderM3EShapeFamily shapeFamily;
|
||||
final SliderM3EDensity density;
|
||||
final bool? showValueIndicator;
|
||||
|
||||
final Widget? startIcon;
|
||||
final Widget? endIcon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = sliderThemeM3E(
|
||||
context,
|
||||
size: size,
|
||||
emphasis: emphasis,
|
||||
shapeFamily: shapeFamily,
|
||||
density: density,
|
||||
showValueIndicator: showValueIndicator ?? false,
|
||||
);
|
||||
|
||||
final slider = Slider(
|
||||
value: value.clamp(min, max),
|
||||
onChanged: onChanged,
|
||||
onChangeStart: onChangeStart,
|
||||
onChangeEnd: onChangeEnd,
|
||||
min: min,
|
||||
max: max,
|
||||
divisions: divisions,
|
||||
label: label,
|
||||
semanticFormatterCallback: semanticLabel != null
|
||||
? (v) => '$semanticLabel ${(100 * ((v - min) / (max - min))).toStringAsFixed(0)}%'
|
||||
: null,
|
||||
);
|
||||
|
||||
if (startIcon == null && endIcon == null) {
|
||||
return SliderTheme(data: theme, child: slider);
|
||||
}
|
||||
|
||||
return SliderTheme(
|
||||
data: theme,
|
||||
child: Row(
|
||||
children: [
|
||||
if (startIcon != null) ...[startIcon!, const SizedBox(width: 8)],
|
||||
Expanded(child: slider),
|
||||
if (endIcon != null) ...[const SizedBox(width: 8), endIcon!],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
21
packages/slider_m3e/lib/src/slider_m3e_widget.dart
Normal file
21
packages/slider_m3e/lib/src/slider_m3e_widget.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_design/m3e_design.dart';
|
||||
|
||||
class SliderM3EWidget extends StatelessWidget {
|
||||
const SliderM3EWidget({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('Slider placeholder', style: m3e.typography.base.titleMedium),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _pascal(String s) => s.split('_').map((p) => p.isEmpty ? '' : (p[0].toUpperCase() + p.substring(1))).join();
|
||||
114
packages/slider_m3e/lib/src/slider_theme_m3e.dart
Normal file
114
packages/slider_m3e/lib/src/slider_theme_m3e.dart
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'slider_tokens_adapter.dart';
|
||||
import 'enums.dart';
|
||||
|
||||
SliderThemeData sliderThemeM3E(
|
||||
BuildContext context, {
|
||||
SliderM3ESize size = SliderM3ESize.medium,
|
||||
SliderM3EEmphasis emphasis = SliderM3EEmphasis.primary,
|
||||
SliderM3EShapeFamily shapeFamily = SliderM3EShapeFamily.round,
|
||||
SliderM3EDensity density = SliderM3EDensity.regular,
|
||||
bool showValueIndicator = false,
|
||||
}) {
|
||||
final t = SliderTokensAdapter(context);
|
||||
final m = t.metrics(density);
|
||||
|
||||
final trackHeight = switch (size) {
|
||||
SliderM3ESize.small => m.trackSmall,
|
||||
SliderM3ESize.medium => m.trackMedium,
|
||||
SliderM3ESize.large => m.trackLarge,
|
||||
};
|
||||
|
||||
final thumbRadius = switch (size) {
|
||||
SliderM3ESize.small => m.thumbSmall,
|
||||
SliderM3ESize.medium => m.thumbMedium,
|
||||
SliderM3ESize.large => m.thumbLarge,
|
||||
};
|
||||
|
||||
final thumbShape = shapeFamily == SliderM3EShapeFamily.round
|
||||
? RoundSliderThumbShape(enabledThumbRadius: thumbRadius)
|
||||
: _SquareThumbShape(side: thumbRadius * 2);
|
||||
|
||||
return SliderTheme.of(context).copyWith(
|
||||
trackHeight: trackHeight,
|
||||
activeTrackColor: t.activeColor(emphasis),
|
||||
inactiveTrackColor: t.inactiveColor(),
|
||||
disabledActiveTrackColor: t.inactiveColor(),
|
||||
disabledInactiveTrackColor: t.inactiveColor(),
|
||||
activeTickMarkColor: t.tickColorActive(emphasis),
|
||||
inactiveTickMarkColor: t.tickColorInactive(),
|
||||
thumbColor: t.thumbColor(emphasis),
|
||||
disabledThumbColor: t.inactiveColor(),
|
||||
overlayColor: t.overlayColor(emphasis),
|
||||
valueIndicatorColor: t.valueIndicatorColor(),
|
||||
valueIndicatorTextStyle: t.valueIndicatorTextStyle(),
|
||||
showValueIndicator: showValueIndicator ? ShowValueIndicator.onDrag : ShowValueIndicator.onlyForDiscrete,
|
||||
thumbShape: thumbShape,
|
||||
overlayShape: RoundSliderOverlayShape(overlayRadius: m.overlayRadius),
|
||||
rangeThumbShape: shapeFamily == SliderM3EShapeFamily.round
|
||||
? const RoundRangeSliderThumbShape()
|
||||
: const _SquareRangeThumbShape(),
|
||||
rangeTrackShape: const RoundedRectRangeSliderTrackShape(),
|
||||
rangeValueIndicatorShape: const PaddleRangeSliderValueIndicatorShape(),
|
||||
valueIndicatorShape: const PaddleSliderValueIndicatorShape(),
|
||||
);
|
||||
}
|
||||
|
||||
class _SquareThumbShape extends SliderComponentShape {
|
||||
const _SquareThumbShape({required this.side});
|
||||
final double side;
|
||||
|
||||
@override
|
||||
Size getPreferredSize(bool isEnabled, bool isDiscrete) => Size.square(side);
|
||||
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
Offset center, {
|
||||
required Animation<double> activationAnimation,
|
||||
required Animation<double> enableAnimation,
|
||||
required bool isDiscrete,
|
||||
required TextPainter? labelPainter,
|
||||
required RenderBox parentBox,
|
||||
required SliderThemeData sliderTheme,
|
||||
required TextDirection textDirection,
|
||||
required double value,
|
||||
required double textScaleFactor,
|
||||
required Size sizeWithOverflow,
|
||||
}) {
|
||||
final canvas = context.canvas;
|
||||
final rect = Rect.fromCenter(center: center, width: side, height: side);
|
||||
final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(4));
|
||||
final paint = Paint()..color = sliderTheme.thumbColor ?? Colors.blue;
|
||||
canvas.drawRRect(rrect, paint);
|
||||
}
|
||||
}
|
||||
|
||||
class _SquareRangeThumbShape extends RangeSliderThumbShape {
|
||||
const _SquareRangeThumbShape();
|
||||
|
||||
@override
|
||||
Size getPreferredSize(bool isEnabled, bool isDiscrete) => const Size(24, 24);
|
||||
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
Offset center, {
|
||||
required Animation<double> activationAnimation,
|
||||
required Animation<double> enableAnimation,
|
||||
bool isDiscrete = false,
|
||||
bool isEnabled = true,
|
||||
bool isOnTop = false,
|
||||
bool isPressed = false,
|
||||
required SliderThemeData sliderTheme,
|
||||
TextDirection textDirection = TextDirection.ltr,
|
||||
Thumb thumb = Thumb.start,
|
||||
}) {
|
||||
final canvas = context.canvas;
|
||||
final side = 24.0;
|
||||
final rect = Rect.fromCenter(center: center, width: side, height: side);
|
||||
final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(4));
|
||||
final paint = Paint()..color = sliderTheme.thumbColor ?? Colors.blue;
|
||||
canvas.drawRRect(rrect, paint);
|
||||
}
|
||||
}
|
||||
83
packages/slider_m3e/lib/src/slider_tokens_adapter.dart
Normal file
83
packages/slider_m3e/lib/src/slider_tokens_adapter.dart
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_design/m3e_design.dart';
|
||||
import 'enums.dart';
|
||||
|
||||
@immutable
|
||||
class _SliderMetrics {
|
||||
final double trackSmall;
|
||||
final double trackMedium;
|
||||
final double trackLarge;
|
||||
final double thumbSmall;
|
||||
final double thumbMedium;
|
||||
final double thumbLarge;
|
||||
final double overlayRadius;
|
||||
final double tickRadius;
|
||||
const _SliderMetrics({
|
||||
required this.trackSmall,
|
||||
required this.trackMedium,
|
||||
required this.trackLarge,
|
||||
required this.thumbSmall,
|
||||
required this.thumbMedium,
|
||||
required this.thumbLarge,
|
||||
required this.overlayRadius,
|
||||
required this.tickRadius,
|
||||
});
|
||||
}
|
||||
|
||||
_SliderMetrics _metricsFor(BuildContext context, SliderM3EDensity density) {
|
||||
// Based on M3 defaults with a slightly more expressive large option.
|
||||
double trS = 2, trM = 4, trL = 6;
|
||||
double thS = 10, thM = 12, thL = 14;
|
||||
double overlay = 20, tick = 2;
|
||||
|
||||
if (density == SliderM3EDensity.compact) {
|
||||
trS -= 0.5; trM -= 0.5; trL -= 1.0;
|
||||
thS -= 1; thM -= 1; thL -= 2;
|
||||
overlay -= 2;
|
||||
}
|
||||
|
||||
return _SliderMetrics(
|
||||
trackSmall: trS,
|
||||
trackMedium: trM,
|
||||
trackLarge: trL,
|
||||
thumbSmall: thS,
|
||||
thumbMedium: thM,
|
||||
thumbLarge: thL,
|
||||
overlayRadius: overlay,
|
||||
tickRadius: tick,
|
||||
);
|
||||
}
|
||||
|
||||
class SliderTokensAdapter {
|
||||
SliderTokensAdapter(this.context);
|
||||
final BuildContext context;
|
||||
|
||||
M3ETheme get _m3e {
|
||||
final t = Theme.of(context);
|
||||
return t.extension<M3ETheme>() ?? M3ETheme.defaults(t.colorScheme);
|
||||
}
|
||||
|
||||
_SliderMetrics metrics(SliderM3EDensity density) => _metricsFor(context, density);
|
||||
|
||||
// Colors
|
||||
Color activeColor(SliderM3EEmphasis e) {
|
||||
switch (e) {
|
||||
case SliderM3EEmphasis.primary: return _m3e.colors.primary;
|
||||
case SliderM3EEmphasis.secondary: return _m3e.colors.secondary;
|
||||
case SliderM3EEmphasis.surface: return _m3e.colors.onSurface;
|
||||
}
|
||||
}
|
||||
Color inactiveColor() => _m3e.colors.onSurface.withValues(alpha: 0.24);
|
||||
Color tickColorActive(SliderM3EEmphasis e) => activeColor(e).withValues(alpha: 0.9);
|
||||
Color tickColorInactive() => _m3e.colors.onSurface.withValues(alpha: 0.38);
|
||||
Color thumbColor(SliderM3EEmphasis e) => activeColor(e);
|
||||
Color overlayColor(SliderM3EEmphasis e) => activeColor(e).withValues(alpha: 0.12);
|
||||
Color valueIndicatorColor() => _m3e.colors.secondaryContainer;
|
||||
TextStyle valueIndicatorTextStyle() => _m3e.type.labelSmall.copyWith(color: _m3e.colors.onSecondaryContainer);
|
||||
|
||||
// Shapes
|
||||
OutlinedBorder containerShape(SliderM3EShapeFamily family) {
|
||||
final set = family == SliderM3EShapeFamily.round ? _m3e.shapes.round : _m3e.shapes.square;
|
||||
return RoundedRectangleBorder(borderRadius: set.md);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue