forked from mirrors/material_3_expressive
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
21
packages/fab_m3e/LICENSE
Normal file
21
packages/fab_m3e/LICENSE
Normal 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.
|
||||
93
packages/fab_m3e/README.md
Normal file
93
packages/fab_m3e/README.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# fab_m3e
|
||||
|
||||
Material 3 **Expressive** Floating Action Buttons for Flutter:
|
||||
|
||||
- **FabM3E**: circular FAB (small / regular / large)
|
||||
- **ExtendedFabM3E**: pill-shaped FAB with label (and optional icon)
|
||||
- **FabMenuM3E**: FAB menu (speed dial) with animated items (up/down/left/right)
|
||||
|
||||
All components read **M3E tokens** from `m3e_design` (ThemeExtension).
|
||||
|
||||
## Monorepo Layout
|
||||
|
||||
```
|
||||
packages/
|
||||
m3e_design/
|
||||
fab_m3e/
|
||||
```
|
||||
|
||||
This package's `pubspec.yaml` references `../m3e_design`.
|
||||
|
||||
## Usage
|
||||
|
||||
### FAB
|
||||
|
||||
```dart
|
||||
import 'package:fab_m3e/fab_m3e.dart';
|
||||
|
||||
FabM3E(
|
||||
icon: const Icon(Icons.add),
|
||||
kind: FabM3EKind.primary,
|
||||
size: FabM3ESize.regular,
|
||||
onPressed: () {},
|
||||
);
|
||||
```
|
||||
|
||||
### Extended FAB
|
||||
|
||||
```dart
|
||||
ExtendedFabM3E(
|
||||
icon: const Icon(Icons.edit),
|
||||
label: const Text('Compose'),
|
||||
kind: FabM3EKind.secondary,
|
||||
onPressed: () {},
|
||||
);
|
||||
```
|
||||
|
||||
### FAB Menu (Speed dial)
|
||||
|
||||
```dart
|
||||
final controller = FabMenuController();
|
||||
|
||||
FabMenuM3E(
|
||||
controller: controller,
|
||||
alignment: Alignment.bottomRight,
|
||||
direction: FabMenuDirection.up,
|
||||
primaryFab: FabM3E(icon: const Icon(Icons.add), onPressed: controller.toggle),
|
||||
items: [
|
||||
FabMenuItem(
|
||||
icon: const Icon(Icons.photo),
|
||||
label: const Text('Photo'),
|
||||
onPressed: () {},
|
||||
),
|
||||
FabMenuItem(
|
||||
icon: const Icon(Icons.note),
|
||||
label: const Text('Note'),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
);
|
||||
```
|
||||
|
||||
## Theming via `m3e_design`
|
||||
|
||||
- Background/foreground colors derive from kind:
|
||||
- `primary` → `primaryContainer` / `onPrimaryContainer`
|
||||
- `secondary` → `secondaryContainer` / `onSecondaryContainer`
|
||||
- `tertiary` → `tertiaryContainer` / `onTertiaryContainer`
|
||||
- `surface` → `surfaceContainerHigh` / `onSurface`
|
||||
- Sizes: **small ≈40dp**, **regular ≈56dp**, **large ≈96dp**
|
||||
- Extended FAB height ≈56dp
|
||||
- Elevations: rest 6, hover 8, pressed 12 (tweak in code or via tokens)
|
||||
- Shapes: `round`/`square` from `m3e_design.shapes` (extended uses StadiumBorder)
|
||||
|
||||
## Notes
|
||||
|
||||
- `FabM3E` uses `RawMaterialButton` to directly inject shape/elevation/colors with tokens.
|
||||
- `ExtendedFabM3E` uses `Material` + `InkWell` with stadium shape and token paddings.
|
||||
- `FabMenuM3E` stacks items near the primary FAB and animates **scale + fade**.
|
||||
- Provide your own `Hero` tags if coordinating transitions across pages.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
7
packages/fab_m3e/lib/fab_m3e.dart
Normal file
7
packages/fab_m3e/lib/fab_m3e.dart
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
library fab_m3e;
|
||||
|
||||
export 'src/enums.dart';
|
||||
export 'src/fab_theme_m3e.dart' show FabTokensAdapter;
|
||||
export 'src/fab_m3e.dart';
|
||||
export 'src/extended_fab_m3e.dart';
|
||||
export 'src/fab_menu_m3e.dart';
|
||||
8
packages/fab_m3e/lib/src/enums.dart
Normal file
8
packages/fab_m3e/lib/src/enums.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
enum FabM3EKind { primary, secondary, tertiary, surface }
|
||||
/// Size mapping follows Material 3: small (≈40), regular (≈56), large (≈96).
|
||||
enum FabM3ESize { small, regular, large }
|
||||
enum FabM3EShapeFamily { round, square }
|
||||
enum FabM3EDensity { regular, compact }
|
||||
|
||||
/// Direction for the FAB menu children to expand.
|
||||
enum FabMenuDirection { up, down, left, right }
|
||||
93
packages/fab_m3e/lib/src/extended_fab_m3e.dart
Normal file
93
packages/fab_m3e/lib/src/extended_fab_m3e.dart
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'enums.dart';
|
||||
import 'fab_theme_m3e.dart';
|
||||
|
||||
class ExtendedFabM3E extends StatelessWidget {
|
||||
const ExtendedFabM3E({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.icon,
|
||||
this.onPressed,
|
||||
this.tooltip,
|
||||
this.heroTag,
|
||||
this.kind = FabM3EKind.primary,
|
||||
this.size = FabM3ESize.regular,
|
||||
this.shapeFamily = FabM3EShapeFamily.round,
|
||||
this.density = FabM3EDensity.regular,
|
||||
this.elevation,
|
||||
this.expand = false,
|
||||
this.semanticLabel,
|
||||
});
|
||||
|
||||
final Widget label;
|
||||
final Widget? icon;
|
||||
final VoidCallback? onPressed;
|
||||
final String? tooltip;
|
||||
final Object? heroTag;
|
||||
final FabM3EKind kind;
|
||||
final FabM3ESize size;
|
||||
final FabM3EShapeFamily shapeFamily;
|
||||
final FabM3EDensity density;
|
||||
final double? elevation;
|
||||
final bool expand;
|
||||
final String? semanticLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = FabTokensAdapter(context);
|
||||
final m = tokens.metrics(density);
|
||||
final bg = tokens.bg(kind);
|
||||
final fg = tokens.fg(kind);
|
||||
final shape = tokens.shape(shapeFamily, size, extended: true);
|
||||
|
||||
final minH = m.extendedHeight;
|
||||
final child = DefaultTextStyle.merge(
|
||||
style: tokens.labelStyle().copyWith(color: fg),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
IconTheme.merge(
|
||||
data: IconThemeData(color: fg, size: m.iconSize), child: icon!),
|
||||
const SizedBox(width: 12)
|
||||
],
|
||||
Flexible(child: label),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final btn = ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: minH),
|
||||
child: Material(
|
||||
shape: shape,
|
||||
color: bg,
|
||||
elevation: elevation ?? m.elevationRest,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
onHover: (_) {},
|
||||
child: Padding(
|
||||
padding: m.extendedPadding,
|
||||
child: Align(alignment: Alignment.center, child: child),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final core = Tooltip(
|
||||
message: tooltip ?? '',
|
||||
preferBelow: false,
|
||||
child: expand ? SizedBox(width: double.infinity, child: btn) : btn,
|
||||
);
|
||||
|
||||
Widget wrapped = core;
|
||||
if (heroTag != null &&
|
||||
context.findAncestorWidgetOfExactType<Hero>() == null) {
|
||||
wrapped = Hero(tag: heroTag!, child: core);
|
||||
}
|
||||
|
||||
if (semanticLabel == null) return wrapped;
|
||||
return Semantics(button: true, label: semanticLabel, child: wrapped);
|
||||
}
|
||||
}
|
||||
88
packages/fab_m3e/lib/src/fab_m3e.dart
Normal file
88
packages/fab_m3e/lib/src/fab_m3e.dart
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'enums.dart';
|
||||
import 'fab_theme_m3e.dart';
|
||||
|
||||
class FabM3E extends StatelessWidget {
|
||||
const FabM3E({
|
||||
super.key,
|
||||
required this.icon,
|
||||
this.onPressed,
|
||||
this.tooltip,
|
||||
this.heroTag,
|
||||
this.kind = FabM3EKind.primary,
|
||||
this.size = FabM3ESize.regular,
|
||||
this.shapeFamily = FabM3EShapeFamily.round,
|
||||
this.density = FabM3EDensity.regular,
|
||||
this.elevation,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.isPrimaryAction = true,
|
||||
this.semanticLabel,
|
||||
});
|
||||
|
||||
final Widget icon;
|
||||
final VoidCallback? onPressed;
|
||||
final String? tooltip;
|
||||
final Object? heroTag;
|
||||
final FabM3EKind kind;
|
||||
final FabM3ESize size;
|
||||
final FabM3EShapeFamily shapeFamily;
|
||||
final FabM3EDensity density;
|
||||
final double? elevation;
|
||||
final FocusNode? focusNode;
|
||||
final bool autofocus;
|
||||
final bool isPrimaryAction;
|
||||
final String? semanticLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = FabTokensAdapter(context);
|
||||
final m = tokens.metrics(density);
|
||||
final bg = tokens.bg(kind);
|
||||
final fg = tokens.fg(kind);
|
||||
final shape = tokens.shape(shapeFamily, size);
|
||||
final double dim = switch (size) {
|
||||
FabM3ESize.small => m.small,
|
||||
FabM3ESize.regular => m.regular,
|
||||
FabM3ESize.large => m.large,
|
||||
};
|
||||
|
||||
final button = SizedBox(
|
||||
width: dim,
|
||||
height: dim,
|
||||
child: RawMaterialButton(
|
||||
onPressed: onPressed,
|
||||
fillColor: bg,
|
||||
elevation: elevation ?? m.elevationRest,
|
||||
hoverElevation: m.elevationHover,
|
||||
highlightElevation: m.elevationPressed,
|
||||
focusElevation: m.elevationHover,
|
||||
shape: shape,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(color: fg, size: m.iconSize),
|
||||
child: icon,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final core = Tooltip(
|
||||
message: tooltip ?? '',
|
||||
preferBelow: false,
|
||||
child: button,
|
||||
);
|
||||
|
||||
// Only wrap with Hero when an explicit tag is provided and there is no ancestor hero.
|
||||
Widget wrapped = core;
|
||||
if (heroTag != null &&
|
||||
context.findAncestorWidgetOfExactType<Hero>() == null) {
|
||||
wrapped = Hero(tag: heroTag!, child: core);
|
||||
}
|
||||
|
||||
if (semanticLabel == null) return wrapped;
|
||||
return Semantics(button: true, label: semanticLabel, child: wrapped);
|
||||
}
|
||||
}
|
||||
21
packages/fab_m3e/lib/src/fab_m3e_widget.dart
Normal file
21
packages/fab_m3e/lib/src/fab_m3e_widget.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_design/m3e_design.dart';
|
||||
|
||||
class FabM3EWidget extends StatelessWidget {
|
||||
const FabM3EWidget({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('Fab placeholder', style: m3e.typography.base.titleMedium),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _pascal(String s) => s.split('_').map((p) => p.isEmpty ? '' : (p[0].toUpperCase() + p.substring(1))).join();
|
||||
247
packages/fab_m3e/lib/src/fab_menu_m3e.dart
Normal file
247
packages/fab_m3e/lib/src/fab_menu_m3e.dart
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_design/m3e_design.dart';
|
||||
|
||||
import 'enums.dart';
|
||||
import 'extended_fab_m3e.dart';
|
||||
|
||||
class FabMenuItem {
|
||||
FabMenuItem({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.semanticLabel,
|
||||
});
|
||||
|
||||
final Widget icon;
|
||||
final Widget label;
|
||||
final VoidCallback onPressed;
|
||||
final String? semanticLabel;
|
||||
}
|
||||
|
||||
class FabMenuController extends ChangeNotifier {
|
||||
bool _open = false;
|
||||
bool get isOpen => _open;
|
||||
void open() {
|
||||
if (!_open) {
|
||||
_open = true;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void close() {
|
||||
if (_open) {
|
||||
_open = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void toggle() {
|
||||
_open = !_open;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class FabMenuM3E extends StatefulWidget {
|
||||
const FabMenuM3E({
|
||||
super.key,
|
||||
required this.primaryFab,
|
||||
required this.items,
|
||||
this.direction = FabMenuDirection.up,
|
||||
this.spacing,
|
||||
this.overlay = true,
|
||||
this.overlayColor,
|
||||
this.controller,
|
||||
this.alignment = Alignment.bottomRight,
|
||||
this.popOnItemTap = true,
|
||||
this.heroTag,
|
||||
});
|
||||
|
||||
/// The FAB that toggles the menu (typically a primary FabM3E or ExtendedFabM3E).
|
||||
final Widget primaryFab;
|
||||
|
||||
/// Menu items displayed when open.
|
||||
final List<FabMenuItem> items;
|
||||
|
||||
/// Direction in which children expand.
|
||||
final FabMenuDirection direction;
|
||||
|
||||
/// Spacing between items.
|
||||
final double? spacing;
|
||||
|
||||
/// Show a scrim overlay behind the menu when open.
|
||||
final bool overlay;
|
||||
final Color? overlayColor;
|
||||
|
||||
/// Optional external controller; if omitted, an internal one is created.
|
||||
final FabMenuController? controller;
|
||||
|
||||
/// Alignment within the Stack (e.g., bottomRight in a Scaffold).
|
||||
final Alignment alignment;
|
||||
|
||||
/// Whether to automatically close the menu when an item is tapped.
|
||||
final bool popOnItemTap;
|
||||
|
||||
final Object? heroTag;
|
||||
|
||||
@override
|
||||
State<FabMenuM3E> createState() => _FabMenuM3EState();
|
||||
}
|
||||
|
||||
class _FabMenuM3EState extends State<FabMenuM3E>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final FabMenuController _controller =
|
||||
widget.controller ?? FabMenuController();
|
||||
late final AnimationController _anim = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
reverseDuration: const Duration(milliseconds: 150),
|
||||
);
|
||||
late final Animation<double> _scale = CurvedAnimation(
|
||||
parent: _anim,
|
||||
curve: Curves.easeOutCubic,
|
||||
reverseCurve: Curves.easeInCubic);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.addListener(_onChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.removeListener(_onChange);
|
||||
if (widget.controller == null) _controller.dispose();
|
||||
_anim.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onChange() {
|
||||
if (_controller.isOpen) {
|
||||
_anim.forward();
|
||||
} else {
|
||||
_anim.reverse();
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sp = context.m3e.spacing; // use spacing scale via context extension
|
||||
final gap = widget.spacing ?? sp.md;
|
||||
|
||||
final children = <Widget>[];
|
||||
|
||||
for (int i = 0; i < widget.items.length; i++) {
|
||||
final item = widget.items[i];
|
||||
final w = _buildMenuItem(context, item);
|
||||
final animatedChild = ScaleTransition(
|
||||
scale: _scale,
|
||||
child: FadeTransition(
|
||||
opacity: _scale,
|
||||
child: w,
|
||||
),
|
||||
);
|
||||
// Ensure Positioned is a direct child of the Stack
|
||||
children.add(_positioned(animatedChild, i, gap));
|
||||
}
|
||||
|
||||
final menu = Stack(
|
||||
alignment: widget.alignment,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
// Primary FAB
|
||||
Align(
|
||||
alignment: widget.alignment,
|
||||
child: _wrapToggle(widget.primaryFab),
|
||||
),
|
||||
// Menu items
|
||||
...children,
|
||||
],
|
||||
);
|
||||
|
||||
final overlay = widget.overlay && _controller.isOpen
|
||||
? Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTap: _controller.toggle,
|
||||
child: ColoredBox(
|
||||
color:
|
||||
widget.overlayColor ?? Colors.black.withValues(alpha: 0.25),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
overlay,
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
ignoring: !_controller.isOpen, child: Container())),
|
||||
menu,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _wrapToggle(Widget child) {
|
||||
final core = GestureDetector(
|
||||
onTap: _controller.toggle,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (widget.heroTag != null && context.findAncestorWidgetOfExactType<Hero>() == null) {
|
||||
return Hero(tag: widget.heroTag!, child: core);
|
||||
}
|
||||
return core;
|
||||
}
|
||||
|
||||
Widget _positioned(Widget child, int index, double gap) {
|
||||
final offset = (index + 1) *
|
||||
(gap +
|
||||
56); // base step; extended affects height, but 56 is a practical default
|
||||
switch (widget.direction) {
|
||||
case FabMenuDirection.up:
|
||||
return Positioned(
|
||||
right: 0,
|
||||
bottom: offset,
|
||||
child: child,
|
||||
);
|
||||
case FabMenuDirection.down:
|
||||
return Positioned(
|
||||
right: 0,
|
||||
top: offset,
|
||||
child: child,
|
||||
);
|
||||
case FabMenuDirection.left:
|
||||
return Positioned(
|
||||
right: offset,
|
||||
bottom: 0,
|
||||
child: child,
|
||||
);
|
||||
case FabMenuDirection.right:
|
||||
return Positioned(
|
||||
left: offset,
|
||||
bottom: 0,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildMenuItem(BuildContext context, FabMenuItem item) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: ExtendedFabM3E(
|
||||
icon: item.icon,
|
||||
label: item.label,
|
||||
onPressed: () {
|
||||
item.onPressed();
|
||||
if (widget.popOnItemTap) _controller.close();
|
||||
},
|
||||
kind: FabM3EKind.surface,
|
||||
size: FabM3ESize.regular,
|
||||
density: FabM3EDensity.regular,
|
||||
semanticLabel: item.semanticLabel,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
106
packages/fab_m3e/lib/src/fab_theme_m3e.dart
Normal file
106
packages/fab_m3e/lib/src/fab_theme_m3e.dart
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_design/m3e_design.dart';
|
||||
import 'enums.dart';
|
||||
|
||||
@immutable
|
||||
class _FabMetrics {
|
||||
final double small;
|
||||
final double regular;
|
||||
final double large;
|
||||
final double extendedHeight;
|
||||
final EdgeInsetsGeometry extendedPadding;
|
||||
final double iconSize;
|
||||
final double elevationRest;
|
||||
final double elevationHover;
|
||||
final double elevationPressed;
|
||||
const _FabMetrics({
|
||||
required this.small,
|
||||
required this.regular,
|
||||
required this.large,
|
||||
required this.extendedHeight,
|
||||
required this.extendedPadding,
|
||||
required this.iconSize,
|
||||
required this.elevationRest,
|
||||
required this.elevationHover,
|
||||
required this.elevationPressed,
|
||||
});
|
||||
}
|
||||
|
||||
_FabMetrics _metricsFor(BuildContext context, FabM3EDensity density) {
|
||||
final theme = Theme.of(context);
|
||||
final m3e = theme.extension<M3ETheme>() ?? M3ETheme.defaults(theme.colorScheme);
|
||||
final sp = m3e.spacing;
|
||||
|
||||
double small = 40;
|
||||
double regular = 56;
|
||||
double large = 96;
|
||||
double extH = 56;
|
||||
double icon = 24;
|
||||
|
||||
if (density == FabM3EDensity.compact) {
|
||||
small -= 4; regular -= 4; large -= 4; extH -= 4;
|
||||
}
|
||||
|
||||
return _FabMetrics(
|
||||
small: small,
|
||||
regular: regular,
|
||||
large: large,
|
||||
extendedHeight: extH,
|
||||
extendedPadding: EdgeInsets.symmetric(horizontal: sp.lg),
|
||||
iconSize: icon,
|
||||
elevationRest: 6.0,
|
||||
elevationHover: 8.0,
|
||||
elevationPressed: 12.0,
|
||||
);
|
||||
}
|
||||
|
||||
class FabTokensAdapter {
|
||||
FabTokensAdapter(this.context);
|
||||
final BuildContext context;
|
||||
|
||||
M3ETheme get _m3e {
|
||||
final t = Theme.of(context);
|
||||
return t.extension<M3ETheme>() ?? M3ETheme.defaults(t.colorScheme);
|
||||
}
|
||||
|
||||
_FabMetrics metrics(FabM3EDensity density) => _metricsFor(context, density);
|
||||
|
||||
// Colors by kind
|
||||
Color bg(FabM3EKind kind) {
|
||||
switch (kind) {
|
||||
case FabM3EKind.primary:
|
||||
return _m3e.colors.primaryContainer;
|
||||
case FabM3EKind.secondary:
|
||||
return _m3e.colors.secondaryContainer;
|
||||
case FabM3EKind.tertiary:
|
||||
return _m3e.colors.tertiaryContainer;
|
||||
case FabM3EKind.surface:
|
||||
return _m3e.colors.surfaceContainerHigh;
|
||||
}
|
||||
}
|
||||
|
||||
Color fg(FabM3EKind kind) {
|
||||
switch (kind) {
|
||||
case FabM3EKind.primary:
|
||||
return _m3e.colors.onPrimaryContainer;
|
||||
case FabM3EKind.secondary:
|
||||
return _m3e.colors.onSecondaryContainer;
|
||||
case FabM3EKind.tertiary:
|
||||
return _m3e.colors.onTertiaryContainer;
|
||||
case FabM3EKind.surface:
|
||||
return _m3e.colors.onSurface;
|
||||
}
|
||||
}
|
||||
|
||||
// Shapes
|
||||
ShapeBorder shape(FabM3EShapeFamily family, FabM3ESize size, {bool extended = false}) {
|
||||
final set = family == FabM3EShapeFamily.round ? _m3e.shapes.round : _m3e.shapes.square;
|
||||
if (extended) return StadiumBorder(side: BorderSide.none);
|
||||
// circular-ish fab: use large radius to approach circle; actual size enforced by constraints
|
||||
final radius = switch (size) { FabM3ESize.small => set.lg, FabM3ESize.regular => set.xl, FabM3ESize.large => set.xl };
|
||||
return RoundedRectangleBorder(borderRadius: radius);
|
||||
}
|
||||
|
||||
// Typography
|
||||
TextStyle labelStyle() => _m3e.type.labelLarge;
|
||||
}
|
||||
29
packages/fab_m3e/melos_fab_m3e.iml
Normal file
29
packages/fab_m3e/melos_fab_m3e.iml
Normal 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>
|
||||
18
packages/fab_m3e/pubspec.yaml
Normal file
18
packages/fab_m3e/pubspec.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
name: fab_m3e
|
||||
description: Material 3 Expressive Floating Action Button (FAB), Extended FAB, and FAB Menu for Flutter 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
|
||||
4
packages/fab_m3e/pubspec_overrides.yaml
Normal file
4
packages/fab_m3e/pubspec_overrides.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# melos_managed_dependency_overrides: m3e_design
|
||||
dependency_overrides:
|
||||
m3e_design:
|
||||
path: ..\\m3e_design
|
||||
7
packages/fab_m3e/test/fab_m3e_test.dart
Normal file
7
packages/fab_m3e/test/fab_m3e_test.dart
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('placeholder', () {
|
||||
expect(3 * 3, 9);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue