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,4 @@
# m3e_design
Design language core for Material 3 Expressive (Flutter).
Provides ThemeExtension and token accessors for color, typography, shapes, spacing, motion.

View file

@ -0,0 +1,10 @@
library m3e_design;
export 'theme/m3e_theme.dart';
export 'tokens/color_tokens.dart';
export 'tokens/motion_tokens.dart';
export 'tokens/shape_tokens.dart';
export 'tokens/spacing_tokens.dart';
export 'tokens/typography_tokens.dart';
export 'utils/build_context_x.dart';
export 'utils/semantics_x.dart';

View file

@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import '../tokens/color_tokens.dart';
import '../tokens/motion_tokens.dart';
import '../tokens/shape_tokens.dart';
import '../tokens/spacing_tokens.dart';
import '../tokens/typography_tokens.dart';
@immutable
class M3ETheme extends ThemeExtension<M3ETheme> {
final M3EColors colors;
final M3ETypography typography;
final M3EShapes shapes;
final M3ESpacing spacing;
final M3EMotion motion;
const M3ETheme({
required this.colors,
required this.typography,
required this.shapes,
required this.spacing,
required this.motion,
});
// Convenience proxy for commonly used text styles in packages (m3e.type.*)
_M3ETypeProxy get type => _M3ETypeProxy(typography);
static M3ETheme defaults(ColorScheme scheme) => M3ETheme(
colors: M3EColors.from(scheme),
typography: M3ETypography.defaultFor(scheme.brightness),
shapes: M3EShapes.expressive(),
spacing: const M3ESpacing.regular(),
motion: const M3EMotion.expressive(),
);
@override
M3ETheme copyWith({
M3EColors? colors,
M3ETypography? typography,
M3EShapes? shapes,
M3ESpacing? spacing,
M3EMotion? motion,
}) =>
M3ETheme(
colors: colors ?? this.colors,
typography: typography ?? this.typography,
shapes: shapes ?? this.shapes,
spacing: spacing ?? this.spacing,
motion: motion ?? this.motion,
);
@override
M3ETheme lerp(covariant M3ETheme? other, double t) {
if (other == null) return this;
return M3ETheme(
colors: M3EColors.lerp(colors, other.colors, t),
typography: M3ETypography.lerp(typography, other.typography, t),
shapes: M3EShapes.lerp(shapes, other.shapes, t),
spacing: M3ESpacing.lerp(spacing, other.spacing, t),
motion: M3EMotion.lerp(motion, other.motion, t),
);
}
}
/// Inject (or replace) the M3ETheme extension on a ThemeData.
ThemeData withM3ETheme(ThemeData base, {M3ETheme? override}) {
// Use any existing M3ETheme, else the provided override, else defaults.
final current = base.extension<M3ETheme>();
final next = override ?? current ?? M3ETheme.defaults(base.colorScheme);
// Merge existing extensions (values) with our M3ETheme, replacing prior ones.
final Iterable<ThemeExtension<dynamic>> existing = base.extensions.values;
final List<ThemeExtension<dynamic>> merged = <ThemeExtension<dynamic>>[];
for (final e in existing) {
if (e is! M3ETheme) {
merged.add(e);
}
}
merged.add(next);
return base.copyWith(extensions: merged);
}
// Internal proxy for typography shortcuts used by components.
class _M3ETypeProxy {
const _M3ETypeProxy(this._t);
final M3ETypography _t;
TextStyle get _empty => const TextStyle();
TextStyle get titleLarge => _t.base.titleLarge ?? _empty;
TextStyle get titleSmall => _t.base.titleSmall ?? _empty;
TextStyle get bodySmall => _t.base.bodySmall ?? _empty;
TextStyle get labelLarge => _t.base.labelLarge ?? _empty;
TextStyle get labelMedium => _t.base.labelMedium ?? _empty;
TextStyle get labelSmall => _t.base.labelSmall ?? _empty;
TextStyle get headlineSmallEmphasized =>
(_t.base.headlineSmall ?? _empty).merge(_t.emphasized.headline);
}

View file

@ -0,0 +1,196 @@
import 'package:flutter/material.dart';
@immutable
class M3EColors {
final Color emphasis;
final Color onEmphasis;
final Color info;
final Color success;
final Color warning;
final Color danger;
final Color surfaceStrong;
final Color onSurfaceStrong;
final Color outlineStrong;
// New: proxy common ColorScheme fields used across packages
final Color primary;
final Color onPrimary;
final Color primaryContainer;
final Color onPrimaryContainer;
final Color secondary;
final Color onSecondary;
final Color secondaryContainer;
final Color onSecondaryContainer;
final Color tertiary;
final Color onTertiary;
final Color tertiaryContainer;
final Color onTertiaryContainer;
final Color surface;
final Color onSurface;
final Color onSurfaceVariant;
final Color error;
final Color onError;
final Color errorContainer;
final Color onErrorContainer;
final Color outline;
final Color outlineVariant;
// New: container surface tokens not always present on older ColorScheme
final Color surfaceContainerHigh;
final Color surfaceContainerLowest;
const M3EColors({
required this.emphasis,
required this.onEmphasis,
required this.info,
required this.success,
required this.warning,
required this.danger,
required this.surfaceStrong,
required this.onSurfaceStrong,
required this.outlineStrong,
// New fields
required this.primary,
required this.onPrimary,
required this.primaryContainer,
required this.onPrimaryContainer,
required this.secondary,
required this.onSecondary,
required this.secondaryContainer,
required this.onSecondaryContainer,
required this.tertiary,
required this.onTertiary,
required this.tertiaryContainer,
required this.onTertiaryContainer,
required this.surface,
required this.onSurface,
required this.onSurfaceVariant,
required this.error,
required this.onError,
required this.errorContainer,
required this.onErrorContainer,
required this.outline,
required this.outlineVariant,
required this.surfaceContainerHigh,
required this.surfaceContainerLowest,
});
factory M3EColors.from(ColorScheme s) {
// Compute container surface variants if not available on the ColorScheme version in use.
// We prefer mild blends that work in both light/dark.
Color computeSurfaceContainerHigh() =>
Color.alphaBlend(s.primary.withValues(alpha: 0.12), s.surface);
Color computeSurfaceContainerLowest() =>
Color.alphaBlend(s.onSurface.withValues(alpha: 0.05), s.surface);
return M3EColors(
emphasis: s.primary,
onEmphasis: s.onPrimary,
info: s.tertiary,
success: Color.alphaBlend(
Colors.green.shade400.withValues(alpha: 0.2), s.primaryContainer),
warning: Color.alphaBlend(
Colors.orange.shade400.withValues(alpha: 0.2), s.secondaryContainer),
danger: Color.alphaBlend(
Colors.red.shade400.withValues(alpha: 0.2), s.errorContainer),
surfaceStrong:
Color.alphaBlend(s.primary.withValues(alpha: 0.06), s.surface),
onSurfaceStrong: s.onSurface,
outlineStrong:
Color.alphaBlend(s.primary.withValues(alpha: 0.40), s.outlineVariant),
// New fields mapped from ColorScheme
primary: s.primary,
onPrimary: s.onPrimary,
primaryContainer: s.primaryContainer,
onPrimaryContainer: s.onPrimaryContainer,
secondary: s.secondary,
onSecondary: s.onSecondary,
secondaryContainer: s.secondaryContainer,
onSecondaryContainer: s.onSecondaryContainer,
tertiary: s.tertiary,
onTertiary: s.onTertiary,
tertiaryContainer: s.tertiaryContainer,
onTertiaryContainer: s.onTertiaryContainer,
surface: s.surface,
onSurface: s.onSurface,
onSurfaceVariant: s.onSurfaceVariant,
error: s.error,
onError: s.onError,
errorContainer: s.errorContainer,
onErrorContainer: s.onErrorContainer,
outline: s.outline,
outlineVariant: s.outlineVariant,
surfaceContainerHigh: (() {
// If the ColorScheme already has a matching field, prefer that via dynamic access; otherwise compute.
try {
final dynamic dyn = s;
final c = dyn.surfaceContainerHigh as Color?;
return c ?? computeSurfaceContainerHigh();
} catch (_) {
return computeSurfaceContainerHigh();
}
})(),
surfaceContainerLowest: (() {
try {
final dynamic dyn = s;
final c = dyn.surfaceContainerLowest as Color?;
return c ?? computeSurfaceContainerLowest();
} catch (_) {
return computeSurfaceContainerLowest();
}
})(),
);
}
static M3EColors lerp(M3EColors a, M3EColors b, double t) => M3EColors(
emphasis: Color.lerp(a.emphasis, b.emphasis, t)!,
onEmphasis: Color.lerp(a.onEmphasis, b.onEmphasis, t)!,
info: Color.lerp(a.info, b.info, t)!,
success: Color.lerp(a.success, b.success, t)!,
warning: Color.lerp(a.warning, b.warning, t)!,
danger: Color.lerp(a.danger, b.danger, t)!,
surfaceStrong: Color.lerp(a.surfaceStrong, b.surfaceStrong, t)!,
onSurfaceStrong: Color.lerp(a.onSurfaceStrong, b.onSurfaceStrong, t)!,
outlineStrong: Color.lerp(a.outlineStrong, b.outlineStrong, t)!,
// New fields
primary: Color.lerp(a.primary, b.primary, t)!,
onPrimary: Color.lerp(a.onPrimary, b.onPrimary, t)!,
primaryContainer:
Color.lerp(a.primaryContainer, b.primaryContainer, t)!,
onPrimaryContainer:
Color.lerp(a.onPrimaryContainer, b.onPrimaryContainer, t)!,
secondary: Color.lerp(a.secondary, b.secondary, t)!,
onSecondary: Color.lerp(a.onSecondary, b.onSecondary, t)!,
secondaryContainer:
Color.lerp(a.secondaryContainer, b.secondaryContainer, t)!,
onSecondaryContainer:
Color.lerp(a.onSecondaryContainer, b.onSecondaryContainer, t)!,
tertiary: Color.lerp(a.tertiary, b.tertiary, t)!,
onTertiary: Color.lerp(a.onTertiary, b.onTertiary, t)!,
tertiaryContainer:
Color.lerp(a.tertiaryContainer, b.tertiaryContainer, t)!,
onTertiaryContainer:
Color.lerp(a.onTertiaryContainer, b.onTertiaryContainer, t)!,
surface: Color.lerp(a.surface, b.surface, t)!,
onSurface: Color.lerp(a.onSurface, b.onSurface, t)!,
onSurfaceVariant:
Color.lerp(a.onSurfaceVariant, b.onSurfaceVariant, t)!,
error: Color.lerp(a.error, b.error, t)!,
onError: Color.lerp(a.onError, b.onError, t)!,
errorContainer: Color.lerp(a.errorContainer, b.errorContainer, t)!,
onErrorContainer:
Color.lerp(a.onErrorContainer, b.onErrorContainer, t)!,
outline: Color.lerp(a.outline, b.outline, t)!,
outlineVariant: Color.lerp(a.outlineVariant, b.outlineVariant, t)!,
surfaceContainerHigh:
Color.lerp(a.surfaceContainerHigh, b.surfaceContainerHigh, t)!,
surfaceContainerLowest:
Color.lerp(a.surfaceContainerLowest, b.surfaceContainerLowest, t)!,
);
}

View file

@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
@immutable
class M3EMotion {
final SpringDescription spatialFast;
final SpringDescription spatialMedium;
final SpringDescription spatialGentle;
final SpringDescription effectsFast;
final SpringDescription effectsMedium;
final Duration fast;
final Duration medium;
final Duration slow;
const M3EMotion({
required this.spatialFast,
required this.spatialMedium,
required this.spatialGentle,
required this.effectsFast,
required this.effectsMedium,
required this.fast,
required this.medium,
required this.slow,
});
const M3EMotion.expressive()
: spatialFast = const SpringDescription(mass: 1, stiffness: 500, damping: 30),
spatialMedium = const SpringDescription(mass: 1, stiffness: 350, damping: 28),
spatialGentle = const SpringDescription(mass: 1, stiffness: 220, damping: 24),
effectsFast = const SpringDescription(mass: 1, stiffness: 420, damping: 32),
effectsMedium = const SpringDescription(mass: 1, stiffness: 280, damping: 28),
fast = const Duration(milliseconds: 150),
medium = const Duration(milliseconds: 250),
slow = const Duration(milliseconds: 400);
static M3EMotion lerp(M3EMotion a, M3EMotion b, double t) => a;
}
class SpringDescription {
final double mass, stiffness, damping;
const SpringDescription({required this.mass, required this.stiffness, required this.damping});
}

View file

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
enum M3EShapeVariant { round, square }
@immutable
class M3EShapeSet {
final BorderRadius xs;
final BorderRadius sm;
final BorderRadius md;
final BorderRadius lg;
final BorderRadius xl;
const M3EShapeSet({required this.xs, required this.sm, required this.md, required this.lg, required this.xl});
}
@immutable
class M3EShapes {
final M3EShapeSet round;
final M3EShapeSet square;
const M3EShapes({required this.round, required this.square});
factory M3EShapes.expressive() => const M3EShapes(
round: M3EShapeSet(
xs: BorderRadius.all(Radius.circular(999)),
sm: BorderRadius.all(Radius.circular(20)),
md: BorderRadius.all(Radius.circular(28)),
lg: BorderRadius.all(Radius.circular(44)),
xl: BorderRadius.all(Radius.circular(64)),
),
square: M3EShapeSet(
xs: BorderRadius.all(Radius.circular(6)),
sm: BorderRadius.all(Radius.circular(8)),
md: BorderRadius.all(Radius.circular(12)),
lg: BorderRadius.all(Radius.circular(16)),
xl: BorderRadius.all(Radius.circular(20)),
),
);
static M3EShapes lerp(M3EShapes a, M3EShapes b, double t) => M3EShapes(
round: _lerpSet(a.round, b.round, t),
square: _lerpSet(a.square, b.square, t),
);
static M3EShapeSet _lerpSet(M3EShapeSet a, M3EShapeSet b, double t) => M3EShapeSet(
xs: BorderRadius.lerp(a.xs, b.xs, t)!,
sm: BorderRadius.lerp(a.sm, b.sm, t)!,
md: BorderRadius.lerp(a.md, b.md, t)!,
lg: BorderRadius.lerp(a.lg, b.lg, t)!,
xl: BorderRadius.lerp(a.xl, b.xl, t)!,
);
}

View file

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
@immutable
class M3ESpacing {
final double xs; // 4
final double sm; // 8
final double md; // 12
final double lg; // 16
final double xl; // 24
final double xxl; // 32
const M3ESpacing({
required this.xs,
required this.sm,
required this.md,
required this.lg,
required this.xl,
required this.xxl,
});
const M3ESpacing.regular()
: xs = 4,
sm = 8,
md = 12,
lg = 16,
xl = 24,
xxl = 32;
static M3ESpacing lerp(M3ESpacing a, M3ESpacing b, double t) => M3ESpacing(
xs: a.xs + (b.xs - a.xs) * t,
sm: a.sm + (b.sm - a.sm) * t,
md: a.md + (b.md - a.md) * t,
lg: a.lg + (b.lg - a.lg) * t,
xl: a.xl + (b.xl - a.xl) * t,
xxl: a.xxl + (b.xxl - a.xxl) * t,
);
}

View file

@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
@immutable
class M3EEmphasized {
final TextStyle display;
final TextStyle headline;
final TextStyle title;
final TextStyle label;
const M3EEmphasized({
required this.display,
required this.headline,
required this.title,
required this.label,
});
static M3EEmphasized forBrightness(Brightness b) {
return const M3EEmphasized(
display: TextStyle(fontWeight: FontWeight.w800, letterSpacing: -0.5),
headline: TextStyle(fontWeight: FontWeight.w700, letterSpacing: -0.25),
title: TextStyle(fontWeight: FontWeight.w700),
label: TextStyle(fontWeight: FontWeight.w700),
);
}
static M3EEmphasized lerp(M3EEmphasized a, M3EEmphasized b, double t) =>
M3EEmphasized(
display: TextStyle.lerp(a.display, b.display, t)!,
headline: TextStyle.lerp(a.headline, b.headline, t)!,
title: TextStyle.lerp(a.title, b.title, t)!,
label: TextStyle.lerp(a.label, b.label, t)!,
);
}
@immutable
class M3ETypography {
final TextTheme base;
final M3EEmphasized emphasized;
const M3ETypography({required this.base, required this.emphasized});
factory M3ETypography.defaultFor(Brightness b) {
// Use a minimal baseline; app's ThemeData will provide fuller TextTheme.
const textTheme = TextTheme();
return M3ETypography(
base: textTheme, emphasized: M3EEmphasized.forBrightness(b));
}
static M3ETypography lerp(M3ETypography a, M3ETypography b, double t) =>
M3ETypography(
base: TextTheme.lerp(a.base, b.base, t),
emphasized: M3EEmphasized.lerp(a.emphasized, b.emphasized, t),
);
}

View file

@ -0,0 +1,7 @@
import 'package:flutter/material.dart';
import '../theme/m3e_theme.dart';
extension BuildContextM3EX on BuildContext {
M3ETheme get m3e =>
Theme.of(this).extension<M3ETheme>() ?? M3ETheme.defaults(Theme.of(this).colorScheme);
}

View file

@ -0,0 +1,5 @@
import 'package:flutter/material.dart';
extension SemanticsX on Widget {
Widget withLabel(String label) => Semantics(label: label, child: this);
}

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,16 @@
name: m3e_design
description: Material 3 Expressive design language for Flutter (tokens, ThemeExtension, motion).
version: 0.1.0
publish_to: none
repository: https://example.com/your-repo
environment:
sdk: ">=3.5.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter