Add Widgetbook setup with theme configuration, analysis options, and initial component use cases

This commit is contained in:
Emily Pauli 2025-10-25 22:59:48 +02:00
commit 80ca8f665a
46 changed files with 6031 additions and 2 deletions

45
widgetbook/.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
widgetbook/.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: "adc901062556672b4138e18a4dc62a4be8f4b3c2"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: android
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: ios
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: linux
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: macos
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: web
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: windows
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
# 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
widgetbook/README.md Normal file
View file

@ -0,0 +1,16 @@
# widgetbook_workspace
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

View file

@ -0,0 +1,37 @@
# INDEX — app_bar_m3e
This index lists all components in the app_bar_m3e package and their Widgetbook variants generated under packages/app_bar_m3e/lib/widgetbook.
## Components and Variants
- AppBarM3E
- default
- with_icon
- with_label
- without_label
- small
- medium
- large
- long_text
- many_actions
- shape_family
- semantics_label
- SliverAppBarM3E
- default
- small
- medium
- large
- pinned
- floating
- snap
- long_text
- many_actions
- shape_family
- density
- semantics_label
## Notes
- Variants use Widgetbook knobs for critical parameters (title, behavior flags), and visual options (density, shape, colors) according to the plan/guide.md.
- Callbacks emit print statements with clear variant context.
- Complex objects are avoided or provided with sensible defaults; future enhancements can expand these demos.

View file

@ -0,0 +1,191 @@
import 'package:app_bar_m3e/app_bar_m3e.dart';
import 'package:flutter/material.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
// Helper: build a list of action buttons with informative prints
List<Widget> _buildActions(int count) {
return List.generate(count, (i) {
return IconButton(
tooltip: 'Action \'${i + 1}\'',
icon:
Icon([Icons.search, Icons.tune, Icons.more_vert, Icons.share][i % 4]),
onPressed: () => print('[AppBarM3E] action ${i + 1} pressed'),
);
});
}
@UseCase(name: 'default', type: AppBarM3E)
Widget buildAppBarM3EDefaultUseCase(BuildContext context) {
final centerTitle =
context.knobs.boolean(label: 'centerTitle', initialValue: false);
final title = context.knobs.string(label: 'title', initialValue: 'App Bar');
final bg = context.knobs.colorOrNull(label: 'backgroundColor');
final fg = context.knobs.colorOrNull(label: 'foregroundColor');
final elevation = context.knobs.doubleOrNull.input(label: 'elevation');
final shapeFamily = context.knobs.object.dropdown<AppBarM3EShapeFamily>(
label: 'shapeFamily',
initialOption: AppBarM3EShapeFamily.round,
options: const [AppBarM3EShapeFamily.round, AppBarM3EShapeFamily.square],
labelBuilder: (v) => v.name,
);
final density = context.knobs.object.dropdown<AppBarM3EDensity>(
label: 'density',
initialOption: AppBarM3EDensity.regular,
options: const [AppBarM3EDensity.regular, AppBarM3EDensity.compact],
labelBuilder: (v) => v.name,
);
final toolbarHeight =
context.knobs.doubleOrNull.input(label: 'toolbarHeight');
final implyLeading = context.knobs
.boolean(label: 'automaticallyImplyLeading', initialValue: true);
final clip = context.knobs.object.dropdown<Clip>(
label: 'clipBehavior',
initialOption: Clip.none,
options: const [
Clip.none,
Clip.hardEdge,
Clip.antiAlias,
Clip.antiAliasWithSaveLayer
],
labelBuilder: (v) => v.name,
);
final actionsCount = context.knobs.int
.slider(label: 'actions', initialValue: 1, min: 0, max: 5);
return AppBarM3E(
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: () => print('[AppBarM3E] leading pressed'),
),
titleText: title,
centerTitle: centerTitle,
backgroundColor: bg,
foregroundColor: fg,
elevation: elevation,
shapeFamily: shapeFamily,
density: density,
toolbarHeight: toolbarHeight,
automaticallyImplyLeading: implyLeading,
clipBehavior: clip,
actions: _buildActions(actionsCount),
);
}
@UseCase(name: 'with_icon', type: AppBarM3E)
Widget buildAppBarM3EWithIconUseCase(BuildContext context) {
return AppBarM3E(
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: () => print('[AppBarM3E] menu tapped'),
),
titleText: context.knobs.string(label: 'title', initialValue: 'With icon'),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () => print('[AppBarM3E] search tapped'),
),
],
);
}
@UseCase(name: 'with_label', type: AppBarM3E)
Widget buildAppBarM3EWithLabelUseCase(BuildContext context) {
return AppBarM3E(
title: Text(
context.knobs.string(label: 'label', initialValue: 'Title Widget'),
overflow: TextOverflow.ellipsis,
),
centerTitle:
context.knobs.boolean(label: 'centerTitle', initialValue: true),
);
}
@UseCase(name: 'without_label', type: AppBarM3E)
Widget buildAppBarM3EWithoutLabelUseCase(BuildContext context) {
return AppBarM3E(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => print('[AppBarM3E] back tapped'),
),
actions: _buildActions(context.knobs.int
.slider(label: 'actions', initialValue: 2, min: 0, max: 6)),
);
}
@UseCase(name: 'small', type: AppBarM3E)
Widget buildAppBarM3ESmallUseCase(BuildContext context) {
final h = context.knobs.double
.slider(label: 'height', initialValue: 56, min: 40, max: 80);
return AppBarM3E(
titleText: 'Small',
toolbarHeight: h,
);
}
@UseCase(name: 'medium', type: AppBarM3E)
Widget buildAppBarM3EMediumUseCase(BuildContext context) {
final h = context.knobs.double
.slider(label: 'height', initialValue: 64, min: 48, max: 96);
return AppBarM3E(
titleText: 'Medium',
toolbarHeight: h,
);
}
@UseCase(name: 'large', type: AppBarM3E)
Widget buildAppBarM3ELargeUseCase(BuildContext context) {
final h = context.knobs.double
.slider(label: 'height', initialValue: 72, min: 56, max: 120);
return AppBarM3E(
titleText: 'Large',
toolbarHeight: h,
);
}
@UseCase(name: 'long_text', type: AppBarM3E)
Widget buildAppBarM3ELongTextUseCase(BuildContext context) {
final repeat = context.knobs.int
.slider(label: 'repeat', initialValue: 3, min: 1, max: 10);
final base =
context.knobs.string(label: 'base', initialValue: 'A very long title');
return AppBarM3E(
titleText: List.filled(repeat, base).join(''),
centerTitle:
context.knobs.boolean(label: 'centerTitle', initialValue: false),
);
}
@UseCase(name: 'many_actions', type: AppBarM3E)
Widget buildAppBarM3EManyActionsUseCase(BuildContext context) {
final count = context.knobs.int
.slider(label: 'actions', initialValue: 4, min: 0, max: 10);
return AppBarM3E(
titleText: 'Many actions',
actions: _buildActions(count),
);
}
@UseCase(name: 'shape_family', type: AppBarM3E)
Widget buildAppBarM3EShapeFamilyUseCase(BuildContext context) {
final family = context.knobs.object.dropdown<AppBarM3EShapeFamily>(
label: 'shapeFamily',
initialOption: AppBarM3EShapeFamily.round,
options: const [AppBarM3EShapeFamily.round, AppBarM3EShapeFamily.square],
labelBuilder: (v) => v.name,
);
return AppBarM3E(
titleText: 'Shape: ${family.name}',
shapeFamily: family,
);
}
@UseCase(name: 'semantics_label', type: AppBarM3E)
Widget buildAppBarM3ESemanticsLabelUseCase(BuildContext context) {
final label = context.knobs
.string(label: 'semanticLabel', initialValue: 'Primary application bar');
return AppBarM3E(
titleText: 'Semantics',
semanticLabel: label,
);
}

View file

@ -0,0 +1,248 @@
import 'package:app_bar_m3e/app_bar_m3e.dart';
import 'package:flutter/material.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
List<Widget> _buildActions(int count) => List.generate(count, (i) {
return IconButton(
tooltip: 'Action \'${i + 1}\'',
icon: Icon(
[Icons.search, Icons.tune, Icons.more_vert, Icons.share][i % 4]),
onPressed: () => print('[SliverAppBarM3E] action ${i + 1} pressed'),
);
});
Widget _demoScrollBody({required SliverAppBarM3E appBar}) {
return CustomScrollView(
slivers: [
appBar,
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text('Item #${index + 1}'),
onTap: () => print('[Sliver] tapped item ${index + 1}'),
),
childCount: 30,
),
),
],
);
}
@UseCase(name: 'default', type: SliverAppBarM3E)
Widget buildSliverAppBarM3EDefaultUseCase(BuildContext context) {
final title =
context.knobs.string(label: 'title', initialValue: 'Sliver App Bar');
final centerTitle =
context.knobs.boolean(label: 'centerTitle', initialValue: false);
final pinned = context.knobs.boolean(label: 'pinned', initialValue: true);
final floating =
context.knobs.boolean(label: 'floating', initialValue: false);
final snap = context.knobs.boolean(label: 'snap', initialValue: false);
final variant = context.knobs.object.dropdown<AppBarM3EVariant>(
label: 'variant',
initialOption: AppBarM3EVariant.medium,
options: const [
AppBarM3EVariant.small,
AppBarM3EVariant.medium,
AppBarM3EVariant.large
],
labelBuilder: (v) => v.name,
);
final shapeFamily = context.knobs.object.dropdown<AppBarM3EShapeFamily>(
label: 'shapeFamily',
initialOption: AppBarM3EShapeFamily.round,
options: const [AppBarM3EShapeFamily.round, AppBarM3EShapeFamily.square],
labelBuilder: (v) => v.name,
);
final density = context.knobs.object.dropdown<AppBarM3EDensity>(
label: 'density',
initialOption: AppBarM3EDensity.regular,
options: const [AppBarM3EDensity.regular, AppBarM3EDensity.compact],
labelBuilder: (v) => v.name,
);
final bg = context.knobs.colorOrNull(label: 'backgroundColor');
final fg = context.knobs.colorOrNull(label: 'foregroundColor');
final actionsCount = context.knobs.int
.slider(label: 'actions', initialValue: 1, min: 0, max: 6);
final appBar = SliverAppBarM3E(
titleText: title,
centerTitle: centerTitle,
pinned: pinned,
floating: floating,
snap: snap,
variant: variant,
shapeFamily: shapeFamily,
density: density,
backgroundColor: bg,
foregroundColor: fg,
actions: _buildActions(actionsCount),
);
return _demoScrollBody(appBar: appBar);
}
@UseCase(name: 'small', type: SliverAppBarM3E)
Widget buildSliverAppBarM3ESmallUseCase(BuildContext context) {
final appBar = SliverAppBarM3E(
titleText: 'Small',
variant: AppBarM3EVariant.small,
);
return _demoScrollBody(appBar: appBar);
}
@UseCase(name: 'medium', type: SliverAppBarM3E)
Widget buildSliverAppBarM3EMediumUseCase(BuildContext context) {
final appBar = SliverAppBarM3E(
titleText: 'Medium',
variant: AppBarM3EVariant.medium,
);
return _demoScrollBody(appBar: appBar);
}
@UseCase(name: 'large', type: SliverAppBarM3E)
Widget buildSliverAppBarM3ELargeUseCase(BuildContext context) {
final appBar = SliverAppBarM3E(
titleText: 'Large',
variant: AppBarM3EVariant.large,
);
return _demoScrollBody(appBar: appBar);
}
@UseCase(name: 'pinned', type: SliverAppBarM3E)
Widget buildSliverAppBarM3EPinnedUseCase(BuildContext context) {
final appBar = SliverAppBarM3E(
titleText: 'Pinned',
pinned: true,
floating: false,
snap: false,
variant: context.knobs.object.dropdown<AppBarM3EVariant>(
label: 'variant',
initialOption: AppBarM3EVariant.medium,
options: const [
AppBarM3EVariant.small,
AppBarM3EVariant.medium,
AppBarM3EVariant.large
],
labelBuilder: (v) => v.name,
),
);
return _demoScrollBody(appBar: appBar);
}
@UseCase(name: 'floating', type: SliverAppBarM3E)
Widget buildSliverAppBarM3EFloatingUseCase(BuildContext context) {
final snap = context.knobs.boolean(label: 'snap', initialValue: false);
final appBar = SliverAppBarM3E(
titleText: 'Floating',
pinned: false,
floating: true,
snap: snap,
variant: context.knobs.object.dropdown<AppBarM3EVariant>(
label: 'variant',
initialOption: AppBarM3EVariant.medium,
options: const [
AppBarM3EVariant.small,
AppBarM3EVariant.medium,
AppBarM3EVariant.large
],
labelBuilder: (v) => v.name,
),
);
return _demoScrollBody(appBar: appBar);
}
@UseCase(name: 'snap', type: SliverAppBarM3E)
Widget buildSliverAppBarM3ESnapUseCase(BuildContext context) {
final appBar = SliverAppBarM3E(
titleText: 'Snap',
pinned: false,
floating: true,
snap: true,
variant: context.knobs.object.dropdown<AppBarM3EVariant>(
label: 'variant',
initialOption: AppBarM3EVariant.medium,
options: const [
AppBarM3EVariant.small,
AppBarM3EVariant.medium,
AppBarM3EVariant.large
],
labelBuilder: (v) => v.name,
),
);
return _demoScrollBody(appBar: appBar);
}
@UseCase(name: 'long_text', type: SliverAppBarM3E)
Widget buildSliverAppBarM3ELongTextUseCase(BuildContext context) {
final repeat = context.knobs.int
.slider(label: 'repeat', initialValue: 3, min: 1, max: 10);
final base =
context.knobs.string(label: 'base', initialValue: 'A very long title');
final appBar = SliverAppBarM3E(
titleText: List.filled(repeat, base).join(''),
variant: context.knobs.object.dropdown<AppBarM3EVariant>(
label: 'variant',
initialOption: AppBarM3EVariant.medium,
options: const [
AppBarM3EVariant.small,
AppBarM3EVariant.medium,
AppBarM3EVariant.large
],
labelBuilder: (v) => v.name,
),
);
return _demoScrollBody(appBar: appBar);
}
@UseCase(name: 'many_actions', type: SliverAppBarM3E)
Widget buildSliverAppBarM3EManyActionsUseCase(BuildContext context) {
final count = context.knobs.int
.slider(label: 'actions', initialValue: 4, min: 0, max: 10);
final appBar = SliverAppBarM3E(
titleText: 'Many actions',
actions: _buildActions(count),
);
return _demoScrollBody(appBar: appBar);
}
@UseCase(name: 'shape_family', type: SliverAppBarM3E)
Widget buildSliverAppBarM3EShapeFamilyUseCase(BuildContext context) {
final family = context.knobs.object.dropdown<AppBarM3EShapeFamily>(
label: 'shapeFamily',
initialOption: AppBarM3EShapeFamily.round,
options: const [AppBarM3EShapeFamily.round, AppBarM3EShapeFamily.square],
labelBuilder: (v) => v.name,
);
final appBar = SliverAppBarM3E(
titleText: 'Shape: ' + family.name,
shapeFamily: family,
);
return _demoScrollBody(appBar: appBar);
}
@UseCase(name: 'density', type: SliverAppBarM3E)
Widget buildSliverAppBarM3EDensityUseCase(BuildContext context) {
final density = context.knobs.object.dropdown<AppBarM3EDensity>(
label: 'density',
initialOption: AppBarM3EDensity.regular,
options: const [AppBarM3EDensity.regular, AppBarM3EDensity.compact],
labelBuilder: (v) => v.name,
);
final appBar = SliverAppBarM3E(
titleText: 'Density: ' + density.name,
density: density,
);
return _demoScrollBody(appBar: appBar);
}
@UseCase(name: 'semantics_label', type: SliverAppBarM3E)
Widget buildSliverAppBarM3ESemanticsLabelUseCase(BuildContext context) {
final label = context.knobs
.string(label: 'semanticLabel', initialValue: 'Scrollable app bar');
final appBar = SliverAppBarM3E(
titleText: 'Semantics',
semanticLabel: label,
);
return _demoScrollBody(appBar: appBar);
}

View file

@ -0,0 +1,21 @@
# Widgetbook Index — button_group_m3e
This index summarizes the Widgetbook variants implemented for the `button_group_m3e` package.
## Components and Variants
- Component: `ButtonGroupM3E`
- Variants:
- default
- with_icon
- without_label
- connected_with_dividers
- vertical
- wrapped_many_items
- equalized_long_text
- empty
## Notes
- Variants use Widgetbook knobs for critical and visual parameters as per plan/guide.md.
- Themes are provided by the global Widgetbook app.
- Callbacks print informative messages to the console.

View file

@ -0,0 +1,363 @@
import 'package:flutter/material.dart';
import 'package:m3e_collection/m3e_collection.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
// GENERATED USE CASES for ButtonGroupM3E per plan/guide.md
// Notes:
// - Themes are provided globally by the Widgetbook app.
// - Use knobs for critical and visual params; callbacks print helpful messages.
// - Complex objects get meaningful defaults with TODOs.
List<Widget> _demoButtonsWithLabels(BuildContext context) => <Widget>[
ButtonM3E(
style: ButtonM3EStyle.elevated,
onPressed: () => debugPrint('Pressed: Primary'),
label: const Text('Elevated'),
),
ButtonM3E(
style: ButtonM3EStyle.outlined,
onPressed: () => debugPrint('Pressed: Secondary'),
label: const Text('Outlined'),
),
ButtonM3E(
style: ButtonM3EStyle.text,
onPressed: () => debugPrint('Pressed: Tertiary'),
label: const Text('Textbutton'),
),
];
List<Widget> _demoButtonsWithIcons(BuildContext context) => <Widget>[
ButtonM3E(
style: ButtonM3EStyle.elevated,
onPressed: () => debugPrint('Pressed: Favorite'),
label: const Text('Favorite'),
icon: const Icon(Icons.favorite_border),
),
ButtonM3E(
style: ButtonM3EStyle.outlined,
onPressed: () => debugPrint('Pressed: Search'),
label: const Text('Search'),
icon: const Icon(Icons.search),
),
ButtonM3E(
style: ButtonM3EStyle.text,
onPressed: () => debugPrint('Pressed: Settings'),
label: const Text('Settings'),
icon: const Icon(Icons.settings_outlined),
),
];
ButtonGroupM3EType _knobType(
BuildContext context, {
ButtonGroupM3EType initial = ButtonGroupM3EType.standard,
}) =>
context.knobs.object.dropdown<ButtonGroupM3EType>(
label: 'type',
initialOption: initial,
options: ButtonGroupM3EType.values,
labelBuilder: (ButtonGroupM3EType v) => v.name,
);
ButtonGroupM3EShape _knobShape(
BuildContext context, {
ButtonGroupM3EShape initial = ButtonGroupM3EShape.round,
}) =>
context.knobs.object.dropdown<ButtonGroupM3EShape>(
label: 'shape',
initialOption: initial,
options: ButtonGroupM3EShape.values,
labelBuilder: (ButtonGroupM3EShape v) => v.name,
);
ButtonGroupM3ESize _knobSize(
BuildContext context, {
ButtonGroupM3ESize initial = ButtonGroupM3ESize.md,
}) =>
context.knobs.object.dropdown<ButtonGroupM3ESize>(
label: 'size',
initialOption: initial,
options: ButtonGroupM3ESize.values,
labelBuilder: (ButtonGroupM3ESize v) => v.name,
);
ButtonGroupM3EDensity _knobDensity(
BuildContext context, {
ButtonGroupM3EDensity initial = ButtonGroupM3EDensity.regular,
}) =>
context.knobs.object.dropdown<ButtonGroupM3EDensity>(
label: 'density',
initialOption: initial,
options: ButtonGroupM3EDensity.values,
labelBuilder: (ButtonGroupM3EDensity v) => v.name,
);
Axis _knobDirection(BuildContext context, {Axis initial = Axis.horizontal}) =>
context.knobs.object.dropdown<Axis>(
label: 'direction',
initialOption: initial,
options: const <Axis>[Axis.horizontal, Axis.vertical],
labelBuilder: (Axis v) => v.name,
);
WrapAlignment _knobWrapAlignment(
BuildContext context, {
String label = 'alignment',
WrapAlignment initial = WrapAlignment.start,
}) =>
context.knobs.object.dropdown<WrapAlignment>(
label: label,
initialOption: initial,
options: WrapAlignment.values,
labelBuilder: (WrapAlignment v) => v.name,
);
WrapCrossAlignment _knobCrossAlignment(
BuildContext context, {
WrapCrossAlignment initial = WrapCrossAlignment.center,
}) =>
context.knobs.object.dropdown<WrapCrossAlignment>(
label: 'crossAxisAlignment',
initialOption: initial,
options: WrapCrossAlignment.values,
labelBuilder: (WrapCrossAlignment v) => v.name,
);
Clip _knobClipBehavior(BuildContext context, {Clip initial = Clip.none}) =>
context.knobs.object.dropdown<Clip>(
label: 'clipBehavior',
initialOption: initial,
options: Clip.values,
labelBuilder: (Clip v) => v.name,
);
@UseCase(name: 'default', type: ButtonGroupM3E)
Widget buildButtonGroupM3EUseCase(BuildContext context) {
final ButtonGroupM3EType type = _knobType(context);
final ButtonGroupM3EShape shape = _knobShape(context);
final ButtonGroupM3ESize size = _knobSize(context);
final ButtonGroupM3EDensity density = _knobDensity(context);
final Axis direction = _knobDirection(context);
final bool wrap = context.knobs.boolean(label: 'wrap', initialValue: false);
final bool showDividers = context.knobs.boolean(
label: 'showDividers (connected only)',
initialValue: false,
);
final bool equalizeWidths = context.knobs.boolean(
label: 'equalizeWidths',
initialValue: false,
);
final double spacing = context.knobs.double.slider(
label: 'spacing',
initialValue: 8,
min: 0,
max: 32,
divisions: 32,
);
final double runSpacing = context.knobs.double.slider(
label: 'runSpacing',
initialValue: 8,
min: 0,
max: 32,
divisions: 32,
);
final Color? dividerColor = context.knobs.colorOrNull(
label: 'dividerColor',
initialValue: null,
);
final double dividerThickness = context.knobs.double.slider(
label: 'dividerThickness',
initialValue: 1,
min: 0.5,
max: 2.0,
divisions: 15,
);
final String? semanticLabel = context.knobs.stringOrNull(
label: 'semanticLabel',
initialValue: 'Action group',
);
final WrapAlignment alignment =
_knobWrapAlignment(context, label: 'alignment');
final WrapAlignment runAlignment =
_knobWrapAlignment(context, label: 'runAlignment');
final WrapCrossAlignment cross = _knobCrossAlignment(context);
final Clip clip = _knobClipBehavior(context);
final String contentMode = context.knobs.object.dropdown<String>(
label: 'content',
initialOption: 'with_label',
options: const <String>['with_label', 'with_icon', 'without_label'],
labelBuilder: (String v) => v,
);
late final List<Widget> children;
switch (contentMode) {
case 'with_icon':
children = _demoButtonsWithIcons(context);
break;
default:
children = _demoButtonsWithLabels(context);
}
return Center(
child: ButtonGroupM3E(
type: type,
shape: shape,
size: size,
density: density,
direction: direction,
wrap: wrap,
spacing: spacing,
runSpacing: runSpacing,
alignment: alignment,
runAlignment: runAlignment,
crossAxisAlignment: cross,
showDividers: showDividers,
dividerColor: dividerColor,
dividerThickness: dividerThickness,
equalizeWidths: equalizeWidths,
semanticLabel: semanticLabel,
clipBehavior: clip,
children: children,
),
);
}
@UseCase(name: 'connected', type: ButtonGroupM3E)
Widget buildButtonGroupM3EConnectedUseCase(BuildContext context) {
return Center(
child: ButtonGroupM3E(
type: ButtonGroupM3EType.connected,
shape: _knobShape(context),
size: _knobSize(context),
density: _knobDensity(context),
direction: _knobDirection(context),
equalizeWidths: context.knobs.boolean(
label: 'equalizeWidths',
initialValue: false,
),
children: _demoButtonsWithLabels(context),
),
);
}
@UseCase(name: 'vertical', type: ButtonGroupM3E)
Widget buildButtonGroupM3EVerticalUseCase(BuildContext context) {
return Center(
child: ButtonGroupM3E(
direction: Axis.vertical,
type: _knobType(context),
shape: _knobShape(context),
size: _knobSize(context),
density: _knobDensity(context),
spacing: context.knobs.double.slider(
label: 'spacing',
initialValue: 8,
min: 0,
max: 32,
divisions: 32,
),
children: _demoButtonsWithLabels(context),
),
);
}
@UseCase(name: 'wrapped_many_items', type: ButtonGroupM3E)
Widget buildButtonGroupM3EWrappedManyItemsUseCase(BuildContext context) {
final int count = context.knobs.int.slider(
label: 'item count',
initialValue: 12,
min: 0,
max: 40,
divisions: 40,
);
final List<Widget> items = List<Widget>.generate(count, (int i) {
return OutlinedButton(
onPressed: () => debugPrint('Pressed: Item #$i'),
child: Text('Item $i'),
);
});
return Center(
child: SizedBox(
width: 360,
child: ButtonGroupM3E(
type: _knobType(context),
shape: _knobShape(context),
size: _knobSize(context),
density: _knobDensity(context),
direction: _knobDirection(context),
wrap: true,
spacing: context.knobs.double.slider(
label: 'spacing',
initialValue: 8,
min: 0,
max: 24,
divisions: 24,
),
runSpacing: context.knobs.double.slider(
label: 'runSpacing',
initialValue: 8,
min: 0,
max: 24,
divisions: 24,
),
alignment: _knobWrapAlignment(context, label: 'alignment'),
runAlignment: _knobWrapAlignment(context, label: 'runAlignment'),
crossAxisAlignment: _knobCrossAlignment(context),
children: items,
),
),
);
}
@UseCase(name: 'equalized_long_text', type: ButtonGroupM3E)
Widget buildButtonGroupM3EEqualizedLongTextUseCase(BuildContext context) {
final List<Widget> children = <Widget>[
ElevatedButton(
onPressed: () => debugPrint('Pressed: Very long primary label'),
child: const Text('Very long primary label'),
),
OutlinedButton(
onPressed: () => debugPrint('Pressed: Short'),
child: const Text('Short'),
),
TextButton(
onPressed: () => debugPrint('Pressed: Mid length'),
child: const Text('Mid length'),
),
];
return Center(
child: ButtonGroupM3E(
type: _knobType(context),
shape: _knobShape(context),
size: _knobSize(context),
density: _knobDensity(context),
equalizeWidths: true,
children: children,
),
);
}
@UseCase(name: 'empty', type: ButtonGroupM3E)
Widget buildButtonGroupM3EEmptyUseCase(BuildContext context) {
// Boundary: no children should layout to zero size.
return const Center(
child: ButtonGroupM3E(children: <Widget>[]),
);
}
@UseCase(name: 'with_icon', type: ButtonGroupM3E)
Widget buildButtonGroupM3EWithIconUseCase(BuildContext context) {
return Center(
child: ButtonGroupM3E(
type: _knobType(context),
shape: _knobShape(context),
size: _knobSize(context),
density: _knobDensity(context),
direction: _knobDirection(context),
children: _demoButtonsWithIcons(context),
),
);
}

View file

@ -0,0 +1,24 @@
# Widgetbook Index — button_m3e
This index lists the ButtonM3E component and all provided Widgetbook variants.
## Components and Variants
- ButtonM3E
- default
- filled
- tonal
- elevated
- outlined
- text
- disabled
- focused
- selected
- with_icon
- long_text
- empty_label
## Notes
- Critical parameters are exposed via knobs: size, shape, enabled, toggleable, selected, label text, icon selection.
- Interaction states can be previewed using knobs and, where needed, simulated via WidgetStatesController (focused, selected).
- Themes are injected globally by the Widgetbook app.

View file

@ -0,0 +1,230 @@
import 'package:button_m3e/button_m3e.dart';
import 'package:flutter/material.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
// GENERATED USE CASES for ButtonM3E per plan/guide.md
// Notes:
// - Themes are provided globally by the Widgetbook app.
// - Use knobs for critical and visual params; callbacks print helpful messages.
// - Complex objects get meaningful defaults with TODOs.
Widget _buildDemo(
BuildContext context, {
required ButtonM3EStyle style,
bool? forceEnabled,
bool forceFocused = false,
bool? forceToggleable,
bool? forceSelected,
bool forceEmptyLabel = false,
bool forceLongText = false,
}) {
// Visual/critical knobs
final ButtonM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: ButtonM3ESize.md,
options: ButtonM3ESize.values,
labelBuilder: (v) => v.name,
);
final ButtonM3EShape shape = context.knobs.object.dropdown(
label: 'shape',
initialOption: ButtonM3EShape.round,
options: ButtonM3EShape.values,
labelBuilder: (v) => v.name,
);
final bool enabled = forceEnabled ??
context.knobs.boolean(label: 'enabled', initialValue: true);
final bool toggleable = forceToggleable ??
context.knobs.boolean(label: 'toggleable', initialValue: false);
final bool selected = forceSelected ??
context.knobs.boolean(label: 'selected', initialValue: false);
final bool withIcon =
context.knobs.boolean(label: 'with_icon', initialValue: false);
final String iconChoice = context.knobs.object.dropdown(
label: 'icon',
initialOption: 'favorite',
options: const ['favorite', 'add', 'download', 'none'],
labelBuilder: (v) => v,
);
// Content knobs
String labelText;
if (forceEmptyLabel) {
labelText = '';
} else if (forceLongText) {
labelText = context.knobs.string(
label: 'label (long)',
initialValue:
'This is a very long label to test truncation and layout behavior',
);
} else {
labelText = context.knobs.string(label: 'label', initialValue: 'Button');
}
// Misc
final bool smallPaddingDeprecated24 = context.knobs.boolean(
label: 'smallPaddingDeprecated24',
initialValue: false,
);
// States controller for focused/selected demos
final statesController = WidgetStatesController();
if (forceFocused) {
statesController.update(WidgetState.focused, true);
}
if (selected && !toggleable) {
statesController.update(WidgetState.selected, true);
}
// Compute icon based on knobs
Widget? icon;
if (withIcon && iconChoice != 'none') {
switch (iconChoice) {
case 'favorite':
icon = const Icon(Icons.favorite);
break;
case 'add':
icon = const Icon(Icons.add);
break;
case 'download':
icon = const Icon(Icons.download);
break;
default:
icon = null;
}
}
return Center(
child: ButtonM3E(
style: style,
size: size,
shape: shape,
enabled: enabled,
toggleable: toggleable,
selected: selected,
onSelectedChange: toggleable
? (val) => debugPrint('ButtonM3E onSelectedChange: newValue=$val')
: null,
onPressed: () => debugPrint(
'ButtonM3E onPressed: style=${style.name}, size=${size.name}, shape=${shape.name}'),
label: Text(labelText),
icon: icon,
smallPaddingDeprecated24: smallPaddingDeprecated24,
statesController: statesController,
),
);
}
@UseCase(name: 'default', type: ButtonM3E)
Widget buildButtonM3EUseCase(BuildContext context) {
// Default uses filled style; other params adjustable with knobs
return _buildDemo(context, style: ButtonM3EStyle.filled);
}
@UseCase(name: 'filled', type: ButtonM3E)
Widget buildButtonM3EFilledUseCase(BuildContext context) {
return _buildDemo(context, style: ButtonM3EStyle.filled);
}
@UseCase(name: 'tonal', type: ButtonM3E)
Widget buildButtonM3ETonalUseCase(BuildContext context) {
return _buildDemo(context, style: ButtonM3EStyle.tonal);
}
@UseCase(name: 'elevated', type: ButtonM3E)
Widget buildButtonM3EElevatedUseCase(BuildContext context) {
return _buildDemo(context, style: ButtonM3EStyle.elevated);
}
@UseCase(name: 'outlined', type: ButtonM3E)
Widget buildButtonM3EOutlinedUseCase(BuildContext context) {
return _buildDemo(context, style: ButtonM3EStyle.outlined);
}
@UseCase(name: 'text', type: ButtonM3E)
Widget buildButtonM3ETextUseCase(BuildContext context) {
return _buildDemo(context, style: ButtonM3EStyle.text);
}
@UseCase(name: 'disabled', type: ButtonM3E)
Widget buildButtonM3EDisabledUseCase(BuildContext context) {
// Force disabled regardless of knob default
return _buildDemo(context, style: ButtonM3EStyle.filled, forceEnabled: false);
}
@UseCase(name: 'focused', type: ButtonM3E)
Widget buildButtonM3EFocusedUseCase(BuildContext context) {
// Preview focused visuals using WidgetStatesController
return _buildDemo(context, style: ButtonM3EStyle.filled, forceFocused: true);
}
@UseCase(name: 'selected', type: ButtonM3E)
Widget buildButtonM3ESelectedUseCase(BuildContext context) {
// Force toggleable + selected for selection visuals
return _buildDemo(
context,
style: ButtonM3EStyle.filled,
forceToggleable: true,
forceSelected: true,
);
}
@UseCase(name: 'with_icon', type: ButtonM3E)
Widget buildButtonM3EWithIconUseCase(BuildContext context) {
// Use knobs to enable icon
return _buildDemo(context, style: ButtonM3EStyle.filled);
}
@UseCase(name: 'long_text', type: ButtonM3E)
Widget buildButtonM3ELongTextUseCase(BuildContext context) {
return _buildDemo(
context,
style: ButtonM3EStyle.filled,
forceLongText: true,
);
}
@UseCase(name: 'empty_label', type: ButtonM3E)
Widget buildButtonM3EEmptyLabelUseCase(BuildContext context) {
// Boundary case: empty label when icon is present or not
return _buildDemo(
context,
style: ButtonM3EStyle.filled,
forceEmptyLabel: true,
);
}
@UseCase(name: 'sizes', type: ButtonM3E)
Widget buildButtonM3ESizesUseCase(BuildContext context) {
final ButtonM3EStyle style = context.knobs.object.dropdown(
label: 'style',
initialOption: ButtonM3EStyle.filled,
options: ButtonM3EStyle.values,
labelBuilder: (v) => v.name,
);
final ButtonM3EShape shape = context.knobs.object.dropdown(
label: 'shape',
initialOption: ButtonM3EShape.round,
options: ButtonM3EShape.values,
labelBuilder: (v) => v.name,
);
final bool withIcon =
context.knobs.boolean(label: 'with_icon', initialValue: false);
return Wrap(
spacing: 16,
runSpacing: 16,
children: ButtonM3ESize.values.map((s) {
return ButtonM3E(
style: style,
size: s,
shape: shape,
onPressed: () => debugPrint('Pressed size: ${s.name}'),
icon: withIcon ? const Icon(Icons.check) : null,
label: Text('Size: ${s.name}'),
);
}).toList(),
);
}

View file

@ -0,0 +1,44 @@
# Widgetbook Index — fab_m3e
This index lists all Widgetbook use cases for the fab_m3e package.
## Components and Variants
### FabM3E
- default
- disabled
- small
- large
- secondary
- tertiary
- surface
- square_shape
- compact_density
- focused
### ExtendedFabM3E
- default
- with_icon
- without_label
- disabled
- expand
- long_text
- secondary
- square_shape
- compact_density
### FabMenuM3E
- default
- initially_open
- direction_down
- direction_left
- direction_right
- overlay_off
- spacing_tight
- many_items
- empty_items
Notes:
- Each use case uses Widgetbook knobs for critical and visual parameters.
- Callbacks print informative messages to the console.
- Complex parameters are configured with meaningful defaults; TODOs to expand configuration may be added later as needed.

View file

@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
import 'package:fab_m3e/fab_m3e.dart';
Widget _buildExtendedFab(
BuildContext context, {
required FabM3EKind kind,
Widget? icon,
String labelText = 'Create',
FabM3ESize size = FabM3ESize.regular,
FabM3EShapeFamily shape = FabM3EShapeFamily.round,
FabM3EDensity density = FabM3EDensity.regular,
bool enabled = true,
bool expand = false,
double? elevation,
String? tooltip,
Object? heroTag,
String? semanticLabel,
}) {
final selectedKind = context.knobs.object.dropdown<FabM3EKind>(
label: 'kind',
initialOption: kind,
options: FabM3EKind.values,
labelBuilder: (v) => v.name,
);
final selectedSize = context.knobs.object.dropdown<FabM3ESize>(
label: 'size',
initialOption: size,
options: FabM3ESize.values,
labelBuilder: (v) => v.name,
);
final selectedShape = context.knobs.object.dropdown<FabM3EShapeFamily>(
label: 'shape',
initialOption: shape,
options: FabM3EShapeFamily.values,
labelBuilder: (v) => v.name,
);
final selectedDensity = context.knobs.object.dropdown<FabM3EDensity>(
label: 'density',
initialOption: density,
options: FabM3EDensity.values,
labelBuilder: (v) => v.name,
);
final label = context.knobs.string(label: 'label', initialValue: labelText);
final withIcon = context.knobs.boolean(label: 'with_icon', initialValue: icon != null);
final isEnabled = context.knobs.boolean(label: 'enabled', initialValue: enabled);
final shouldExpand = context.knobs.boolean(label: 'expand', initialValue: expand);
final dblElevation = context.knobs.doubleOrNull.slider(
label: 'elevation',
initialValue: elevation,
min: 0.0,
max: 24.0,
divisions: 24,
);
final tip = context.knobs.stringOrNull(label: 'tooltip', initialValue: tooltip);
final semLabel = context.knobs.stringOrNull(label: 'semanticLabel', initialValue: semanticLabel);
final useHero = context.knobs.boolean(label: 'wrap in Hero', initialValue: heroTag != null);
final Widget fab = ExtendedFabM3E(
label: Text(label, overflow: TextOverflow.ellipsis),
icon: withIcon ? const Icon(Icons.add) : null,
onPressed: isEnabled ? () => print('ExtendedFabM3E pressed (kind=$selectedKind, size=$selectedSize)') : null,
tooltip: tip,
heroTag: useHero ? (heroTag ?? 'extended-fab-hero') : null,
kind: selectedKind,
size: selectedSize,
shapeFamily: selectedShape,
density: selectedDensity,
expand: shouldExpand,
elevation: dblElevation,
semanticLabel: semLabel,
);
return Center(
child: SizedBox(width: shouldExpand ? 360 : null, child: fab),
);
}
@widgetbook.UseCase(name: 'default', type: ExtendedFabM3E)
Widget buildExtendedFabM3EUseCase(BuildContext context) {
return _buildExtendedFab(context, kind: FabM3EKind.primary, icon: const Icon(Icons.add));
}
@widgetbook.UseCase(name: 'with_icon', type: ExtendedFabM3E)
Widget buildExtendedFabM3EWithIconUseCase(BuildContext context) {
return _buildExtendedFab(context, kind: FabM3EKind.primary, icon: const Icon(Icons.add));
}
@widgetbook.UseCase(name: 'without_label', type: ExtendedFabM3E)
Widget buildExtendedFabM3EWithoutLabelUseCase(BuildContext context) {
return _buildExtendedFab(context, kind: FabM3EKind.primary, labelText: '');
}
@widgetbook.UseCase(name: 'disabled', type: ExtendedFabM3E)
Widget buildExtendedFabM3EDisabledUseCase(BuildContext context) {
return _buildExtendedFab(context, kind: FabM3EKind.primary, icon: const Icon(Icons.add), enabled: false);
}
@widgetbook.UseCase(name: 'expand', type: ExtendedFabM3E)
Widget buildExtendedFabM3EExpandUseCase(BuildContext context) {
return _buildExtendedFab(context, kind: FabM3EKind.primary, icon: const Icon(Icons.add), expand: true);
}
@widgetbook.UseCase(name: 'long_text', type: ExtendedFabM3E)
Widget buildExtendedFabM3ELongTextUseCase(BuildContext context) {
return _buildExtendedFab(context, kind: FabM3EKind.primary, icon: const Icon(Icons.add), labelText: 'Compose a very long descriptive label that may overflow');
}
@widgetbook.UseCase(name: 'secondary', type: ExtendedFabM3E)
Widget buildExtendedFabM3ESecondaryUseCase(BuildContext context) {
return _buildExtendedFab(context, kind: FabM3EKind.secondary, icon: const Icon(Icons.edit));
}
@widgetbook.UseCase(name: 'square_shape', type: ExtendedFabM3E)
Widget buildExtendedFabM3ESquareShapeUseCase(BuildContext context) {
return _buildExtendedFab(context, kind: FabM3EKind.primary, icon: const Icon(Icons.add), shape: FabM3EShapeFamily.square);
}
@widgetbook.UseCase(name: 'compact_density', type: ExtendedFabM3E)
Widget buildExtendedFabM3ECompactDensityUseCase(BuildContext context) {
return _buildExtendedFab(context, kind: FabM3EKind.primary, icon: const Icon(Icons.add), density: FabM3EDensity.compact);
}

View file

@ -0,0 +1,131 @@
import 'package:fab_m3e/fab_m3e.dart';
import 'package:flutter/material.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
Widget _buildFab(
BuildContext context, {
required FabM3EKind kind,
FabM3ESize size = FabM3ESize.regular,
FabM3EShapeFamily shape = FabM3EShapeFamily.round,
FabM3EDensity density = FabM3EDensity.regular,
bool enabled = true,
bool autofocus = false,
double? elevation,
String? tooltip,
Object? heroTag,
String? semanticLabel,
}) {
final selectedKind = context.knobs.object.dropdown<FabM3EKind>(
label: 'kind',
initialOption: kind,
options: FabM3EKind.values,
labelBuilder: (v) => v.name,
);
final selectedSize = context.knobs.object.dropdown<FabM3ESize>(
label: 'size',
initialOption: size,
options: FabM3ESize.values,
labelBuilder: (v) => v.name,
);
final selectedShape = context.knobs.object.dropdown<FabM3EShapeFamily>(
label: 'shape',
initialOption: shape,
options: FabM3EShapeFamily.values,
labelBuilder: (v) => v.name,
);
final selectedDensity = context.knobs.object.dropdown<FabM3EDensity>(
label: 'density',
initialOption: density,
options: FabM3EDensity.values,
labelBuilder: (v) => v.name,
);
final isEnabled =
context.knobs.boolean(label: 'enabled', initialValue: enabled);
final isAutofocus =
context.knobs.boolean(label: 'autofocus', initialValue: autofocus);
final dblElevation = context.knobs.doubleOrNull.slider(
label: 'elevation',
initialValue: elevation,
min: 0.0,
max: 24.0,
divisions: 24,
);
final tip =
context.knobs.stringOrNull(label: 'tooltip', initialValue: tooltip);
final semLabel = context.knobs
.stringOrNull(label: 'semanticLabel', initialValue: semanticLabel);
final useHero = context.knobs
.boolean(label: 'wrap in Hero', initialValue: heroTag != null);
final Widget fab = FabM3E(
icon: const Icon(Icons.add),
onPressed: isEnabled
? () => print('FabM3E pressed (kind=$selectedKind, size=$selectedSize)')
: null,
tooltip: tip,
heroTag: useHero ? (heroTag ?? 'fab-hero') : null,
kind: selectedKind,
size: selectedSize,
shapeFamily: selectedShape,
density: selectedDensity,
elevation: dblElevation,
autofocus: isAutofocus,
semanticLabel: semLabel,
);
return Center(child: fab);
}
@widgetbook.UseCase(name: 'default', type: FabM3E)
Widget buildFabM3EUseCase(BuildContext context) {
return _buildFab(context, kind: FabM3EKind.primary);
}
@widgetbook.UseCase(name: 'disabled', type: FabM3E)
Widget buildFabM3EDisabledUseCase(BuildContext context) {
return _buildFab(context, kind: FabM3EKind.primary, enabled: false);
}
@widgetbook.UseCase(name: 'small', type: FabM3E)
Widget buildFabM3ESmallUseCase(BuildContext context) {
return _buildFab(context, kind: FabM3EKind.primary, size: FabM3ESize.small);
}
@widgetbook.UseCase(name: 'large', type: FabM3E)
Widget buildFabM3ELargeUseCase(BuildContext context) {
return _buildFab(context, kind: FabM3EKind.primary, size: FabM3ESize.large);
}
@widgetbook.UseCase(name: 'secondary', type: FabM3E)
Widget buildFabM3ESecondaryUseCase(BuildContext context) {
return _buildFab(context, kind: FabM3EKind.secondary);
}
@widgetbook.UseCase(name: 'tertiary', type: FabM3E)
Widget buildFabM3ETertiaryUseCase(BuildContext context) {
return _buildFab(context, kind: FabM3EKind.tertiary);
}
@widgetbook.UseCase(name: 'surface', type: FabM3E)
Widget buildFabM3ESurfaceUseCase(BuildContext context) {
return _buildFab(context, kind: FabM3EKind.surface);
}
@widgetbook.UseCase(name: 'square_shape', type: FabM3E)
Widget buildFabM3ESquareShapeUseCase(BuildContext context) {
return _buildFab(context,
kind: FabM3EKind.primary, shape: FabM3EShapeFamily.square);
}
@widgetbook.UseCase(name: 'compact_density', type: FabM3E)
Widget buildFabM3ECompactDensityUseCase(BuildContext context) {
return _buildFab(context,
kind: FabM3EKind.primary, density: FabM3EDensity.compact);
}
@widgetbook.UseCase(name: 'focused', type: FabM3E)
Widget buildFabM3EFocusedUseCase(BuildContext context) {
// Emphasize focus by enabling autofocus
return _buildFab(context, kind: FabM3EKind.primary, autofocus: true);
}

View file

@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
import 'package:fab_m3e/fab_m3e.dart';
FabMenuM3E _buildMenu(
BuildContext context, {
required List<FabMenuItem> items,
FabMenuDirection direction = FabMenuDirection.up,
bool overlay = true,
double? spacing,
Alignment alignment = Alignment.bottomRight,
bool popOnItemTap = true,
Object? heroTag,
bool initiallyOpen = false,
}) {
final selectedDirection = context.knobs.object.dropdown<FabMenuDirection>(
label: 'direction',
initialOption: direction,
options: FabMenuDirection.values,
labelBuilder: (v) => v.name,
);
final showOverlay = context.knobs.boolean(label: 'overlay', initialValue: overlay);
final gap = context.knobs.doubleOrNull.slider(
label: 'spacing',
initialValue: spacing,
min: 0,
max: 48,
divisions: 24,
);
final alignOpt = context.knobs.object.dropdown<String>(
label: 'alignment',
initialOption: 'bottomRight',
options: const ['bottomRight', 'bottomLeft', 'topRight', 'topLeft', 'center'],
labelBuilder: (v) => v,
);
final Alignment align = switch (alignOpt) {
'bottomLeft' => Alignment.bottomLeft,
'topRight' => Alignment.topRight,
'topLeft' => Alignment.topLeft,
'center' => Alignment.center,
_ => Alignment.bottomRight,
};
final popOnTap = context.knobs.boolean(label: 'popOnItemTap', initialValue: popOnItemTap);
final useHero = context.knobs.boolean(label: 'wrap in Hero', initialValue: heroTag != null);
final controller = FabMenuController();
if (initiallyOpen) controller.open();
return FabMenuM3E(
primaryFab: FabM3E(
icon: AnimatedRotation(
duration: const Duration(milliseconds: 200),
turns: controller.isOpen ? 0.125 : 0,
child: const Icon(Icons.add),
),
onPressed: controller.toggle,
heroTag: useHero ? (heroTag ?? 'fab-menu-hero') : null,
),
items: items,
direction: selectedDirection,
overlay: showOverlay,
spacing: gap,
controller: controller,
alignment: align,
popOnItemTap: popOnTap,
);
}
List<FabMenuItem> _makeItems(BuildContext context, int count) {
return List.generate(count, (i) {
return FabMenuItem(
icon: Icon([Icons.share, Icons.edit, Icons.delete, Icons.copy][i % 4]),
label: Text('Action ${i + 1}'),
onPressed: () => print('Menu item ${i + 1} pressed'),
semanticLabel: 'Menu action ${i + 1}',
);
});
}
@widgetbook.UseCase(name: 'default', type: FabMenuM3E)
Widget buildFabMenuM3EUseCase(BuildContext context) {
return Stack(
children: [
Positioned.fill(
child: Container(),
),
_buildMenu(
context,
items: _makeItems(context, 3),
direction: FabMenuDirection.up,
overlay: true,
),
],
);
}
@widgetbook.UseCase(name: 'initially_open', type: FabMenuM3E)
Widget buildFabMenuM3EInitiallyOpenUseCase(BuildContext context) {
return _buildMenu(
context,
items: _makeItems(context, 3),
direction: FabMenuDirection.up,
overlay: true,
initiallyOpen: true,
);
}
@widgetbook.UseCase(name: 'direction_down', type: FabMenuM3E)
Widget buildFabMenuM3EDirectionDownUseCase(BuildContext context) {
return _buildMenu(context, items: _makeItems(context, 3), direction: FabMenuDirection.down);
}
@widgetbook.UseCase(name: 'direction_left', type: FabMenuM3E)
Widget buildFabMenuM3EDirectionLeftUseCase(BuildContext context) {
return _buildMenu(context, items: _makeItems(context, 3), direction: FabMenuDirection.left);
}
@widgetbook.UseCase(name: 'direction_right', type: FabMenuM3E)
Widget buildFabMenuM3EDirectionRightUseCase(BuildContext context) {
return _buildMenu(context, items: _makeItems(context, 3), direction: FabMenuDirection.right);
}
@widgetbook.UseCase(name: 'overlay_off', type: FabMenuM3E)
Widget buildFabMenuM3EOverlayOffUseCase(BuildContext context) {
return _buildMenu(context, items: _makeItems(context, 3), overlay: false);
}
@widgetbook.UseCase(name: 'spacing_tight', type: FabMenuM3E)
Widget buildFabMenuM3ESpacingTightUseCase(BuildContext context) {
return _buildMenu(context, items: _makeItems(context, 3), spacing: 4);
}
@widgetbook.UseCase(name: 'many_items', type: FabMenuM3E)
Widget buildFabMenuM3EManyItemsUseCase(BuildContext context) {
return _buildMenu(context, items: _makeItems(context, 8));
}
@widgetbook.UseCase(name: 'empty_items', type: FabMenuM3E)
Widget buildFabMenuM3EEmptyItemsUseCase(BuildContext context) {
return _buildMenu(context, items: const []);
}

View file

@ -0,0 +1,34 @@
# Widgetbook Index — icon_button_m3e
This index summarizes all Widgetbook use cases implemented for the `icon_button_m3e` package.
- Package path: `packages/icon_button_m3e`
- Component inventory:
- IconButtonM3E
## IconButtonM3E — Variants
| Variant name | Description |
|----------------------|-------------|
| default | Baseline interactive demo with knobs for variant, size, shape, width, tooltip, selected, feedback, and badge. |
| disabled | Disabled state across variants/sizes. |
| standard | Visual style preset: standard. |
| filled | Visual style preset: filled. |
| tonal | Visual style preset: tonal. |
| outlined | Visual style preset: outlined. |
| sizes | Shows all sizes (xs/sm/md/lg/xl) for the selected style/shape/width. |
| with_badge | Demonstrates numeric badges via knob (0 shows dot). |
| with_badge_string | Demonstrates string badges such as "NEW". |
| badge_edge_cases | Boundary cases: dot (0), small (1), large (99), very large (clamped). |
| with_tooltip | Tooltip demo, including long tooltip text toggle. |
| shapes | Round vs square shapes. |
| widths | Narrow, default, wide widths (disabled for comparison). |
| selected | Toggle selected state with alternate selectedIcon. |
| focused | Autofocused control for focus visuals. |
| error_badge_type | Error case: invalid `badgeValue` type (asserts in debug). |
## Notes
- Themes are provided globally by the Widgetbook app; use cases do not configure themes.
- Knobs are used for critical and visual parameters following `plan/guide.md`.
- Callbacks log informative messages with `debugPrint` to aid interactive testing.
- Complex objects use sensible defaults and include TODO notes when applicable.

View file

@ -0,0 +1,516 @@
// ignore_for_file: depend_on_referenced_packages
import 'package:flutter/material.dart';
import 'package:icon_button_m3e/icon_button_m3e.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
// GENERATED USE CASES for IconButtonM3E per plan/guide.md
// Notes:
// - Themes are provided globally by the Widgetbook app.
// - Use knobs for critical and visual params; callbacks print helpful messages.
// - Complex objects get meaningful defaults with TODOs.
@UseCase(name: 'default', type: IconButtonM3E)
Widget buildIconButtonM3EUseCase(BuildContext context) {
final bool isToggle = context.knobs.boolean(
label: 'is toggle (provides selected state)',
initialValue: true,
);
final bool selected = isToggle
? context.knobs.boolean(label: 'isSelected', initialValue: false)
: false;
final IconButtonM3EVariant variant = context.knobs.object.dropdown(
label: 'variant',
initialOption: IconButtonM3EVariant.standard,
options: IconButtonM3EVariant.values,
labelBuilder: (v) => v.name,
);
final IconButtonM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: IconButtonM3ESize.sm,
options: IconButtonM3ESize.values,
labelBuilder: (v) => v.name,
);
final IconButtonM3EShapeVariant shape = context.knobs.object.dropdown(
label: 'shape',
initialOption: IconButtonM3EShapeVariant.round,
options: IconButtonM3EShapeVariant.values,
labelBuilder: (v) => v.name,
);
final IconButtonM3EWidth width = context.knobs.object.dropdown(
label: 'width',
initialOption: IconButtonM3EWidth.defaultWidth,
options: IconButtonM3EWidth.values,
labelBuilder: (v) => v.name,
);
final String? tooltip = context.knobs.stringOrNull(
label: 'tooltip',
initialValue: 'Favorite',
);
final bool enableFeedback = context.knobs.boolean(
label: 'enableFeedback',
initialValue: true,
);
final bool showBadge = context.knobs.boolean(
label: 'show badge',
initialValue: false,
);
final bool badgeIsNumber = context.knobs.boolean(
label: 'badge is number (off = string)',
initialValue: true,
);
final Object? badgeValue = showBadge
? (badgeIsNumber
? context.knobs.int.slider(
label: 'badge count',
initialValue: 1,
min: 0,
max: 999,
divisions: 30,
)
: context.knobs.string(label: 'badge label', initialValue: 'NEW'))
: null;
return Center(
child: IconButtonM3E(
icon: const Icon(Icons.favorite_border),
selectedIcon: const Icon(Icons.favorite),
onPressed: () => debugPrint('IconButtonM3E pressed'),
isSelected: isToggle ? selected : null,
tooltip: tooltip,
enableFeedback: enableFeedback,
variant: variant,
size: size,
shape: shape,
width: width,
badgeValue: badgeValue,
),
);
}
@UseCase(name: 'disabled', type: IconButtonM3E)
Widget buildIconButtonM3EDisabledUseCase(BuildContext context) {
final IconButtonM3EVariant variant = context.knobs.object.dropdown(
label: 'variant',
initialOption: IconButtonM3EVariant.standard,
options: IconButtonM3EVariant.values,
labelBuilder: (v) => v.name,
);
final IconButtonM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: IconButtonM3ESize.sm,
options: IconButtonM3ESize.values,
labelBuilder: (v) => v.name,
);
final IconButtonM3EShapeVariant shape = context.knobs.object.dropdown(
label: 'shape',
initialOption: IconButtonM3EShapeVariant.round,
options: IconButtonM3EShapeVariant.values,
labelBuilder: (v) => v.name,
);
final IconButtonM3EWidth width = context.knobs.object.dropdown(
label: 'width',
initialOption: IconButtonM3EWidth.defaultWidth,
options: IconButtonM3EWidth.values,
labelBuilder: (v) => v.name,
);
return Center(
child: IconButtonM3E(
icon: const Icon(Icons.volume_up),
selectedIcon: const Icon(Icons.volume_off),
onPressed: null, // disabled state
variant: variant,
size: size,
shape: shape,
width: width,
tooltip: 'Disabled button',
),
);
}
@UseCase(name: 'standard', type: IconButtonM3E)
Widget buildIconButtonM3EStandardUseCase(BuildContext context) {
return _buildStyleDemo(context, IconButtonM3EVariant.standard);
}
@UseCase(name: 'filled', type: IconButtonM3E)
Widget buildIconButtonM3EFilledUseCase(BuildContext context) {
return _buildStyleDemo(context, IconButtonM3EVariant.filled);
}
@UseCase(name: 'tonal', type: IconButtonM3E)
Widget buildIconButtonM3ETonalUseCase(BuildContext context) {
return _buildStyleDemo(context, IconButtonM3EVariant.tonal);
}
@UseCase(name: 'outlined', type: IconButtonM3E)
Widget buildIconButtonM3EOutlinedUseCase(BuildContext context) {
return _buildStyleDemo(context, IconButtonM3EVariant.outlined);
}
Widget _buildStyleDemo(BuildContext context, IconButtonM3EVariant variant) {
final IconButtonM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: IconButtonM3ESize.sm,
options: IconButtonM3ESize.values,
labelBuilder: (v) => v.name,
);
final IconButtonM3EShapeVariant shape = context.knobs.object.dropdown(
label: 'shape',
initialOption: IconButtonM3EShapeVariant.round,
options: IconButtonM3EShapeVariant.values,
labelBuilder: (v) => v.name,
);
final IconButtonM3EWidth width = context.knobs.object.dropdown(
label: 'width',
initialOption: IconButtonM3EWidth.defaultWidth,
options: IconButtonM3EWidth.values,
labelBuilder: (v) => v.name,
);
final bool isSelected = context.knobs.boolean(
label: 'isSelected (toggle) ',
initialValue: false,
);
final String iconChoice = context.knobs.object.dropdown(
label: 'icon',
initialOption: 'favorite',
options: const ['favorite', 'alarm', 'share', 'settings'],
labelBuilder: (s) => s,
);
IconData data;
switch (iconChoice) {
case 'alarm':
data = Icons.alarm;
break;
case 'share':
data = Icons.share;
break;
case 'settings':
data = Icons.settings;
break;
default:
data = isSelected ? Icons.favorite : Icons.favorite_border;
}
return Center(
child: IconButtonM3E(
icon: Icon(data),
selectedIcon: const Icon(Icons.check),
isSelected: isSelected,
onPressed: () => debugPrint('Pressed style=$variant size=$size'),
variant: variant,
size: size,
shape: shape,
width: width,
tooltip: 'Icon: $iconChoice',
),
);
}
@UseCase(name: 'sizes', type: IconButtonM3E)
Widget buildIconButtonM3ESizesUseCase(BuildContext context) {
final IconButtonM3EVariant variant = context.knobs.object.dropdown(
label: 'variant',
initialOption: IconButtonM3EVariant.standard,
options: IconButtonM3EVariant.values,
labelBuilder: (v) => v.name,
);
final IconButtonM3EShapeVariant shape = context.knobs.object.dropdown(
label: 'shape',
initialOption: IconButtonM3EShapeVariant.round,
options: IconButtonM3EShapeVariant.values,
labelBuilder: (v) => v.name,
);
final IconButtonM3EWidth width = context.knobs.object.dropdown(
label: 'width',
initialOption: IconButtonM3EWidth.defaultWidth,
options: IconButtonM3EWidth.values,
labelBuilder: (v) => v.name,
);
return Wrap(
spacing: 16,
runSpacing: 16,
children: IconButtonM3ESize.values.map((s) {
return IconButtonM3E(
icon: const Icon(Icons.star_border),
selectedIcon: const Icon(Icons.star),
onPressed: () => debugPrint('Size $s pressed'),
variant: variant,
size: s,
shape: shape,
width: width,
tooltip: 'size: ${s.name}',
);
}).toList(),
);
}
@UseCase(name: 'with_badge', type: IconButtonM3E)
Widget buildIconButtonM3EWithBadgeUseCase(BuildContext context) {
final int count = context.knobs.int.slider(
label: 'count',
initialValue: 1,
min: 0,
max: 999,
divisions: 20,
);
final IconButtonM3EVariant variant = context.knobs.object.dropdown(
label: 'variant',
initialOption: IconButtonM3EVariant.standard,
options: IconButtonM3EVariant.values,
labelBuilder: (v) => v.name,
);
final IconButtonM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: IconButtonM3ESize.sm,
options: IconButtonM3ESize.values,
labelBuilder: (v) => v.name,
);
return Center(
child: IconButtonM3E(
icon: const Icon(Icons.mail_outline),
selectedIcon: const Icon(Icons.mail),
onPressed: () => debugPrint('Mail pressed'),
variant: variant,
size: size,
badgeValue: count,
tooltip: 'Inbox',
),
);
}
@UseCase(name: 'with_badge_string', type: IconButtonM3E)
Widget buildIconButtonM3EWithBadgeStringUseCase(BuildContext context) {
final String label = context.knobs.string(
label: 'badge label',
initialValue: 'NEW',
);
final IconButtonM3EVariant variant = context.knobs.object.dropdown(
label: 'variant',
initialOption: IconButtonM3EVariant.filled,
options: IconButtonM3EVariant.values,
labelBuilder: (v) => v.name,
);
return Center(
child: IconButtonM3E(
icon: const Icon(Icons.shopping_bag_outlined),
selectedIcon: const Icon(Icons.shopping_bag),
onPressed: () => debugPrint('Bag pressed'),
variant: variant,
badgeValue: label,
tooltip: 'Cart',
),
);
}
@UseCase(name: 'badge_edge_cases', type: IconButtonM3E)
Widget buildIconButtonM3EBadgeEdgeCasesUseCase(BuildContext context) {
return Wrap(
spacing: 16,
runSpacing: 16,
children: const [
IconButtonM3E(
icon: Icon(Icons.notifications_none),
badgeValue: 0, // dot badge
tooltip: '0 = dot',
),
IconButtonM3E(
icon: Icon(Icons.notifications_none),
badgeValue: 1,
tooltip: '1',
),
IconButtonM3E(
icon: Icon(Icons.notifications_none),
badgeValue: 99,
tooltip: '99',
),
IconButtonM3E(
icon: Icon(Icons.notifications_none),
badgeValue: 999999, // clamped
tooltip: 'big number',
),
],
);
}
@UseCase(name: 'with_tooltip', type: IconButtonM3E)
Widget buildIconButtonM3EWithTooltipUseCase(BuildContext context) {
final String text = context.knobs.string(
label: 'tooltip',
initialValue: 'Open menu',
);
final bool long = context.knobs.boolean(
label: 'use long tooltip',
initialValue: false,
);
final String value = long
? 'This is a very long tooltip that demonstrates how the control behaves with extended descriptive text. '
'Try hovering or long-pressing to read the entire message.'
: text;
return Center(
child: IconButtonM3E(
icon: const Icon(Icons.more_vert),
onPressed: () => debugPrint('More pressed'),
tooltip: value,
),
);
}
@UseCase(name: 'shapes', type: IconButtonM3E)
Widget buildIconButtonM3EShapesUseCase(BuildContext context) {
final IconButtonM3EVariant variant = context.knobs.object.dropdown(
label: 'variant',
initialOption: IconButtonM3EVariant.standard,
options: IconButtonM3EVariant.values,
labelBuilder: (v) => v.name,
);
final IconButtonM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: IconButtonM3ESize.sm,
options: IconButtonM3ESize.values,
labelBuilder: (v) => v.name,
);
return Wrap(
spacing: 16,
children: [
IconButtonM3E(
icon: const Icon(Icons.crop_5_4),
onPressed: () {},
variant: variant,
size: size,
shape: IconButtonM3EShapeVariant.round,
tooltip: 'round',
),
IconButtonM3E(
icon: const Icon(Icons.crop_square),
onPressed: () {},
variant: variant,
size: size,
shape: IconButtonM3EShapeVariant.square,
tooltip: 'square',
),
],
);
}
@UseCase(name: 'widths', type: IconButtonM3E)
Widget buildIconButtonM3EWidthsUseCase(BuildContext context) {
final IconButtonM3EVariant variant = context.knobs.object.dropdown(
label: 'variant',
initialOption: IconButtonM3EVariant.standard,
options: IconButtonM3EVariant.values,
labelBuilder: (v) => v.name,
);
final IconButtonM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: IconButtonM3ESize.sm,
options: IconButtonM3ESize.values,
labelBuilder: (v) => v.name,
);
return Wrap(
spacing: 16,
children: const [
IconButtonM3E(
icon: Icon(Icons.aspect_ratio),
onPressed: null,
width: IconButtonM3EWidth.narrow,
tooltip: 'narrow (disabled) ',
),
IconButtonM3E(
icon: Icon(Icons.aspect_ratio),
onPressed: null,
width: IconButtonM3EWidth.defaultWidth,
tooltip: 'default (disabled)',
),
IconButtonM3E(
icon: Icon(Icons.aspect_ratio),
onPressed: null,
width: IconButtonM3EWidth.wide,
tooltip: 'wide (disabled)',
),
],
);
}
@UseCase(name: 'selected', type: IconButtonM3E)
Widget buildIconButtonM3ESelectedUseCase(BuildContext context) {
final IconButtonM3EVariant variant = context.knobs.object.dropdown(
label: 'variant',
initialOption: IconButtonM3EVariant.standard,
options: IconButtonM3EVariant.values,
labelBuilder: (v) => v.name,
);
final IconButtonM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: IconButtonM3ESize.sm,
options: IconButtonM3ESize.values,
labelBuilder: (v) => v.name,
);
return Center(
child: IconButtonM3E(
icon: const Icon(Icons.radio_button_unchecked),
selectedIcon: const Icon(Icons.radio_button_checked),
isSelected: true,
onPressed: () => debugPrint('Selected toggled'),
variant: variant,
size: size,
tooltip: 'Selected = true',
),
);
}
@UseCase(name: 'focused', type: IconButtonM3E)
Widget buildIconButtonM3EFocusedUseCase(BuildContext context) {
final IconButtonM3EVariant variant = context.knobs.object.dropdown(
label: 'variant',
initialOption: IconButtonM3EVariant.standard,
options: IconButtonM3EVariant.values,
labelBuilder: (v) => v.name,
);
final IconButtonM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: IconButtonM3ESize.sm,
options: IconButtonM3ESize.values,
labelBuilder: (v) => v.name,
);
return Center(
child: Focus(
autofocus: true,
child: IconButtonM3E(
icon: const Icon(Icons.center_focus_strong),
onPressed: () => debugPrint('Focused pressed'),
variant: variant,
size: size,
tooltip: 'Autofocused control',
),
),
);
}
@UseCase(name: 'error_badge_type', type: IconButtonM3E)
Widget buildIconButtonM3EErrorBadgeTypeUseCase(BuildContext context) {
// This intentionally passes an unsupported type to badgeValue to demonstrate
// assertion behavior in debug mode. In release/profile, this simply omits the badge.
// TODO: Consider exposing a strongly-typed API for badges to avoid runtime asserts.
final Object invalid = const [1, 2, 3];
return Center(
child: IconButtonM3E(
icon: const Icon(Icons.error_outline),
onPressed: () => debugPrint('Pressed error_badge_type case'),
badgeValue: invalid, // will assert in debug
tooltip: 'Invalid badge value (List)',
),
);
}

View file

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:m3e_collection/m3e_collection.dart';
import 'package:widgetbook/widgetbook.dart';
Widget _buildIconButtonDemo(
BuildContext context, {
required IconButtonM3EVariant variant,
}) {
final size = context.knobs.object.dropdown<IconButtonM3ESize>(
label: 'size',
initialOption: IconButtonM3ESize.md,
options: IconButtonM3ESize.values,
labelBuilder: (v) => v.name,
);
final shape = context.knobs.object.dropdown<IconButtonM3EShapeVariant>(
label: 'shape',
initialOption: IconButtonM3EShapeVariant.round,
options: IconButtonM3EShapeVariant.values,
labelBuilder: (v) => v.name,
);
final width = context.knobs.object.dropdown<IconButtonM3EWidth>(
label: 'width',
initialOption: IconButtonM3EWidth.defaultWidth,
options: IconButtonM3EWidth.values,
labelBuilder: (v) => v.name,
);
final isSelected =
context.knobs.boolean(label: 'selected', initialValue: false);
final tooltip =
context.knobs.string(label: 'tooltip', initialValue: 'Favorite');
final badgeMode = context.knobs.object.dropdown<String>(
label: 'badge',
initialOption: 'none',
options: const ['none', 'dot', 'count', 'text'],
labelBuilder: (v) => v,
);
Object? badgeValue;
switch (badgeMode) {
case 'dot':
badgeValue = 0; // shows dot per component logic
break;
case 'count':
badgeValue = context.knobs.int.slider(
label: 'count',
initialValue: 3,
min: 0,
max: 120,
divisions: 120,
);
break;
case 'text':
badgeValue =
context.knobs.string(label: 'badgeText', initialValue: 'NEW');
break;
default:
badgeValue = null;
}
return Center(
child: IconButtonM3E(
icon: const Icon(Icons.favorite_border),
selectedIcon: const Icon(Icons.favorite),
isSelected: isSelected,
tooltip: tooltip,
variant: variant,
size: size,
shape: shape,
width: width,
onPressed: () => print('IconButton pressed: variant=$variant'),
badgeValue: badgeValue,
),
);
}

View file

@ -0,0 +1,29 @@
# Widgetbook Index — loading_indicator_m3e
This index lists all components in loading_indicator_m3e and their Widgetbook use case variants.
Component inventory:
- LoadingIndicatorM3E
- ExpressiveLoadingIndicator
## LoadingIndicatorM3E (lib/src/loading_indicator_m3e.dart)
Use case file: lib/widgetbook/loading_indicator_m3e_usecases.dart
- default — Default style with token-based colors and sizing
- contained — Contained visual variant
- sizes — Small/Medium/Large/Tiny via constraints knob
- custom_colors — Active and container colors via color knobs
- with_padding — Padding and size knobs
- with_semantics — semanticsLabel and semanticsValue knobs
- custom_polygons — Choose polygon set and size via knobs
## ExpressiveLoadingIndicator (lib/src/expressive_loading_indicator.dart)
Use case file: lib/widgetbook/expressive_loading_indicator_usecases.dart
- default — Defaults from ProgressIndicatorTheme and widget internals
- sizes — Small/Medium/Large/Tiny via constraints knob
- custom_polygons — Choose polygon set and size
- color_and_semantics — Color and semantics knobs
- edge: invalid polygons (debug assert) — Toggle to provide a single polygon (asserts in debug)
Notes:
- Themes are injected globally by the Widgetbook app; no theme provided here.
- Knobs follow the Comprehensive Knobs API suggested in plan/guide.md.

View file

@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
import 'package:loading_indicator_m3e/loading_indicator_m3e.dart';
import 'package:material_new_shapes/material_new_shapes.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
// GENERATED USE CASES for ExpressiveLoadingIndicator per plan/guide.md
// - Themes are provided globally by the Widgetbook app.
// - Use knobs for critical & visual params.
// - Complex objects get meaningful defaults with TODOs where needed.
BoxConstraints _tight(double size) => BoxConstraints(
minWidth: size,
minHeight: size,
maxWidth: size,
maxHeight: size,
);
List<RoundedPolygon> _polygonSet(String id) {
switch (id) {
case 'cookie→oval':
return [MaterialShapes.cookie9Sided, MaterialShapes.oval];
case 'softBurst→pill→sunny':
return [MaterialShapes.softBurst, MaterialShapes.pill, MaterialShapes.sunny];
case 'triangle→square→pentagon':
return [MaterialShapes.triangle, MaterialShapes.square, MaterialShapes.pentagon];
default:
return [
MaterialShapes.softBurst,
MaterialShapes.cookie9Sided,
MaterialShapes.pentagon,
MaterialShapes.pill,
MaterialShapes.sunny,
MaterialShapes.cookie4Sided,
MaterialShapes.oval,
];
}
}
@UseCase(name: 'default', type: ExpressiveLoadingIndicator)
Widget buildExpressiveLoadingIndicatorUseCase(BuildContext context) {
return const Center(
child: ExpressiveLoadingIndicator(),
);
}
@UseCase(name: 'sizes', type: ExpressiveLoadingIndicator)
Widget buildExpressiveLoadingIndicatorSizesUseCase(BuildContext context) {
final String size = context.knobs.object.dropdown<String>(
label: 'size',
initialOption: 'medium',
options: const ['small', 'medium', 'large', 'tiny (edge)'],
labelBuilder: (v) => v,
);
final double px = switch (size) {
'small' => 36,
'large' => 64,
'tiny (edge)' => 16,
_ => 48,
};
return Center(
child: ExpressiveLoadingIndicator(constraints: _tight(px)),
);
}
@UseCase(name: 'custom_polygons', type: ExpressiveLoadingIndicator)
Widget buildExpressiveLoadingIndicatorCustomPolygonsUseCase(
BuildContext context) {
final String setName = context.knobs.object.dropdown<String>(
label: 'polygon set',
initialOption: 'default',
options: const [
'default',
'cookie→oval',
'softBurst→pill→sunny',
'triangle→square→pentagon',
],
labelBuilder: (v) => v,
);
final double size = context.knobs.double.slider(
label: 'size (px)',
initialValue: 48,
min: 24,
max: 96,
);
final polygons = _polygonSet(setName);
return Center(
child: ExpressiveLoadingIndicator(
polygons: polygons,
constraints: _tight(size),
),
);
}
@UseCase(name: 'color_and_semantics', type: ExpressiveLoadingIndicator)
Widget buildExpressiveLoadingIndicatorColorAndSemanticsUseCase(
BuildContext context) {
final color = context.knobs.color(label: 'color');
final label = context.knobs.string(
label: 'semanticsLabel',
initialValue: 'Loading',
);
final value = context.knobs.string(
label: 'semanticsValue',
initialValue: 'In progress',
);
return Center(
child: ExpressiveLoadingIndicator(
color: color,
semanticsLabel: label,
semanticsValue: value,
),
);
}
@UseCase(name: 'edge: invalid polygons (debug assert)', type: ExpressiveLoadingIndicator)
Widget buildExpressiveLoadingIndicatorInvalidPolygonsUseCase(
BuildContext context) {
final enableInvalid = context.knobs.boolean(
label: 'trigger invalid (single polygon)',
initialValue: false,
);
final size = context.knobs.double.slider(
label: 'size (px)',
initialValue: 48,
min: 24,
max: 96,
);
// Note: When enabled, this will assert in debug as polygons.length must be > 1.
final polygons = enableInvalid
? <RoundedPolygon>[MaterialShapes.oval]
: _polygonSet('default');
return Center(
child: ExpressiveLoadingIndicator(
polygons: polygons,
constraints: _tight(size),
),
);
}

View file

@ -0,0 +1,184 @@
import 'package:flutter/material.dart';
import 'package:loading_indicator_m3e/loading_indicator_m3e.dart';
import 'package:material_new_shapes/material_new_shapes.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
// GENERATED USE CASES for LoadingIndicatorM3E per plan/guide.md
// Notes:
// - Themes are provided globally by the Widgetbook app.
// - Use knobs for critical and visual params; callbacks would print helpful messages (none here).
// - Complex objects get meaningful defaults with TODOs.
BoxConstraints _sizeToTight(double size) => BoxConstraints(
minWidth: size,
minHeight: size,
maxWidth: size,
maxHeight: size,
);
EdgeInsets _paddingAll(double value) => EdgeInsets.all(value);
List<RoundedPolygon> _polygonSet(String id) {
switch (id) {
case 'cookie→oval':
return [
MaterialShapes.cookie9Sided,
MaterialShapes.oval,
];
case 'softBurst→pill→sunny':
return [
MaterialShapes.softBurst,
MaterialShapes.pill,
MaterialShapes.sunny,
];
case 'triangle→square→pentagon':
return [
MaterialShapes.triangle,
MaterialShapes.square,
MaterialShapes.pentagon,
];
default:
// default sequence mirrors the package's internal default
return [
MaterialShapes.softBurst,
MaterialShapes.cookie9Sided,
MaterialShapes.pentagon,
MaterialShapes.pill,
MaterialShapes.sunny,
MaterialShapes.cookie4Sided,
MaterialShapes.oval,
];
}
}
@UseCase(name: 'default', type: LoadingIndicatorM3E)
Widget buildLoadingIndicatorM3EUseCase(BuildContext context) {
// Keep defaults: tokens-based sizing and colors, subtle container.
return const Center(
child: LoadingIndicatorM3E(),
);
}
@UseCase(name: 'contained', type: LoadingIndicatorM3E)
Widget buildLoadingIndicatorM3EContainedUseCase(BuildContext context) {
// Visual family: contained variant uses a stronger container color.
return const Center(
child: LoadingIndicatorM3E(variant: LoadingIndicatorM3EVariant.contained),
);
}
@UseCase(name: 'sizes', type: LoadingIndicatorM3E)
Widget buildLoadingIndicatorM3ESizesUseCase(BuildContext context) {
final String size = context.knobs.object.dropdown<String>(
label: 'size',
initialOption: 'medium',
options: const ['small', 'medium', 'large', 'tiny (edge)'],
labelBuilder: (v) => v,
);
final double px = switch (size) {
'small' => 36,
'large' => 64,
'tiny (edge)' => 16, // boundary case
_ => 48, // medium
};
return Center(
child: LoadingIndicatorM3E(
constraints: _sizeToTight(px),
),
);
}
@UseCase(name: 'custom_colors', type: LoadingIndicatorM3E)
Widget buildLoadingIndicatorM3ECustomColorsUseCase(BuildContext context) {
final variant = context.knobs.object.dropdown<LoadingIndicatorM3EVariant>(
label: 'variant',
initialOption: LoadingIndicatorM3EVariant.defaultStyle,
options: LoadingIndicatorM3EVariant.values,
labelBuilder: (v) => v.name,
);
final active = context.knobs.color(label: 'active color');
final container = context.knobs.color(label: 'container color');
return Center(
child: LoadingIndicatorM3E(
variant: variant,
color: active,
containerColor: container,
),
);
}
@UseCase(name: 'with_padding', type: LoadingIndicatorM3E)
Widget buildLoadingIndicatorM3EWithPaddingUseCase(BuildContext context) {
final double pad = context.knobs.double.slider(
label: 'padding',
initialValue: 8,
min: 0,
max: 32,
);
final double size = context.knobs.double.slider(
label: 'size (px)',
initialValue: 48,
min: 16,
max: 96,
);
return Center(
child: LoadingIndicatorM3E(
padding: _paddingAll(pad),
constraints: _sizeToTight(size),
),
);
}
@UseCase(name: 'with_semantics', type: LoadingIndicatorM3E)
Widget buildLoadingIndicatorM3EWithSemanticsUseCase(BuildContext context) {
final String label = context.knobs.string(
label: 'semanticsLabel',
initialValue: 'Loading content',
);
final String value = context.knobs.string(
label: 'semanticsValue',
initialValue: 'Please wait…',
);
return Center(
child: LoadingIndicatorM3E(
semanticLabel: label,
semanticValue: value,
),
);
}
@UseCase(name: 'custom_polygons', type: LoadingIndicatorM3E)
Widget buildLoadingIndicatorM3ECustomPolygonsUseCase(BuildContext context) {
final String setName = context.knobs.object.dropdown<String>(
label: 'polygon set',
initialOption: 'default',
options: const [
'default',
'cookie→oval',
'softBurst→pill→sunny',
'triangle→square→pentagon',
],
labelBuilder: (v) => v,
);
final double size = context.knobs.double.slider(
label: 'size (px)',
initialValue: 48,
min: 24,
max: 96,
);
final polygons = _polygonSet(setName);
return Center(
child: LoadingIndicatorM3E(
polygons: polygons,
constraints: _sizeToTight(size),
),
);
}

View file

@ -0,0 +1,28 @@
# Widgetbook Index — m3e_design
This package (m3e_design) provides the design system core for Material 3 Expressive: ThemeExtension(s), tokens (colors, typography, spacing, shapes, motion), and utilities. It does not expose standalone user-facing Widgets.
Therefore, no component use cases are generated for this package. If downstream packages want to showcase tokens visually, they should do so in their own Widgetbook contexts using this design system.
## Discovery (Components)
- Scanned packages/m3e_design/lib for public Widgets (classes extending StatelessWidget/StatefulWidget).
- Result: none found — only ThemeExtension, token data classes, and helpers.
```
[ ] Component inventory:
- [ ] (none)
```
## Summary Table
| Component | Variants |
|---|---|
| — No direct widgets in this package — | — |
## Notes
- Themes are globally injected in consuming apps; no themes are provided here for use cases.
- Optional previews (typography, palette, spacing, shapes, motion) could be created as internal demo widgets under widgetbook/ if desired. Not included to keep this package lean and avoid exporting demo-only code.
_Last updated: 2025-10-25_

40
widgetbook/lib/main.dart Normal file
View file

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:m3e_design/m3e_design.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
// Generated by widgetbook_generator
import 'main.directories.g.dart';
void main() {
runApp(const WidgetbookApp());
}
@widgetbook.App()
class WidgetbookApp extends StatelessWidget {
const WidgetbookApp({super.key});
@override
Widget build(BuildContext context) {
final themeLight =
ColorScheme.fromSeed(seedColor: Colors.purple).toM3EThemeData();
final themeDark = ColorScheme.fromSeed(
seedColor: Colors.purple, brightness: Brightness.dark)
.toM3EThemeData();
return Widgetbook.material(
directories: directories,
lightTheme: themeLight,
darkTheme: themeDark,
addons: [
MaterialThemeAddon(
themes: [
WidgetbookTheme(name: 'Light', data: themeLight),
WidgetbookTheme(name: 'Dark', data: themeDark),
],
),
TextScaleAddon(),
],
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,34 @@
# INDEX — navigation_bar_m3e
This index summarizes Widgetbook use cases for components in packages/navigation_bar_m3e.
## Components and Variants
### NavigationBarM3E
- default
- pill_indicator
- underline_indicator
- none_indicator
- small_size
- medium_size
- round_shape
- square_shape
- regular_density
- compact_density
- with_3_destinations
- with_4_destinations
- with_5_destinations
- with_badges
### NavBadgeM3E
- default
- dot
- edge_max_count
- custom_colors
- offset_tweak
- with_semantic_label
Notes
- All use cases follow plan/guide.md: @UseCase annotations and build[Component][Variant]UseCase signatures.
- Knobs are provided for critical parameters and visual options.
- Callbacks print helpful messages where applicable.

View file

@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import 'package:navigation_bar_m3e/navigation_bar_m3e.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
// Helper: Build demo destinations (35), optionally with badges
List<NavigationDestinationM3E> _demoDestinations(
int count, {
bool withBadges = false,
}) {
final clamped = count.clamp(3, 5);
final base = <(IconData, IconData, String)>[
(Icons.home_outlined, Icons.home_rounded, 'Home'),
(Icons.search, Icons.search_rounded, 'Search'),
(Icons.notifications_none_rounded, Icons.notifications_rounded, 'Alerts'),
(Icons.person_outline_rounded, Icons.person_rounded, 'Profile'),
(Icons.settings_outlined, Icons.settings_rounded, 'Settings'),
];
return List.generate(clamped, (i) {
final (icon, selectedIcon, label) = base[i];
return NavigationDestinationM3E(
icon: Icon(icon),
selectedIcon: Icon(selectedIcon),
label: label,
badgeCount: withBadges && i == 2 ? 12 : null,
badgeDot: withBadges && i == 1,
semanticLabel: withBadges ? 'Tab $label with badge' : 'Tab $label',
);
});
}
// A flexible "playground" builder used by the variants below
Widget _buildNavBar(
BuildContext context, {
NavBarM3EIndicatorStyle? indicatorStyle,
NavBarM3ESize? size,
NavBarM3EShapeFamily? shapeFamily,
NavBarM3EDensity? density,
bool? withBadges,
Color? backgroundColor,
Color? indicatorColor,
}) {
final count = context.knobs.int.slider(
label: 'destinations',
initialValue: 4,
min: 3,
max: 5,
);
final idx = context.knobs.int.slider(
label: 'selectedIndex',
initialValue: 0,
min: 0,
max: count - 1,
);
final labelBehavior = context.knobs.object.dropdown<NavBarM3ELabelBehavior>(
label: 'labelBehavior',
initialOption: NavBarM3ELabelBehavior.alwaysShow,
options: const [
NavBarM3ELabelBehavior.alwaysShow,
NavBarM3ELabelBehavior.onlySelected,
NavBarM3ELabelBehavior.alwaysHide,
],
labelBuilder: (v) => v.name,
);
final safeArea = context.knobs.boolean(label: 'safeArea', initialValue: true);
final bg = context.knobs.colorOrNull(label: 'backgroundColor');
final ind = context.knobs.colorOrNull(label: 'indicatorColor');
return NavigationBarM3E(
destinations: _demoDestinations(count, withBadges: withBadges ?? false),
selectedIndex: idx,
onDestinationSelected: (i) =>
debugPrint('NavigationBarM3E: selected index = $i'),
labelBehavior: labelBehavior,
size: size ?? NavBarM3ESize.medium,
shapeFamily: shapeFamily ?? NavBarM3EShapeFamily.round,
density: density ?? NavBarM3EDensity.regular,
indicatorStyle: indicatorStyle ?? NavBarM3EIndicatorStyle.pill,
safeArea: safeArea,
backgroundColor: backgroundColor ?? bg,
indicatorColor: indicatorColor ?? ind,
);
}
@UseCase(name: 'default', type: NavigationBarM3E)
Widget buildNavigationBarM3EDefaultUseCase(BuildContext context) {
return _buildNavBar(context);
}
@UseCase(name: 'playground', type: NavigationBarM3E)
Widget buildNavigationBarM3EPlaygroundUseCase(BuildContext context) {
final indicator = context.knobs.object.dropdown<NavBarM3EIndicatorStyle>(
label: 'indicatorStyle',
initialOption: NavBarM3EIndicatorStyle.pill,
options: const [
NavBarM3EIndicatorStyle.pill,
NavBarM3EIndicatorStyle.underline,
NavBarM3EIndicatorStyle.none,
],
labelBuilder: (v) => v.name,
);
final size = context.knobs.object.dropdown<NavBarM3ESize>(
label: 'size',
initialOption: NavBarM3ESize.medium,
options: const [NavBarM3ESize.small, NavBarM3ESize.medium],
labelBuilder: (v) => v.name,
);
final shape = context.knobs.object.dropdown<NavBarM3EShapeFamily>(
label: 'shapeFamily',
initialOption: NavBarM3EShapeFamily.round,
options: const [NavBarM3EShapeFamily.round, NavBarM3EShapeFamily.square],
labelBuilder: (v) => v.name,
);
final density = context.knobs.object.dropdown<NavBarM3EDensity>(
label: 'density',
initialOption: NavBarM3EDensity.regular,
options: const [NavBarM3EDensity.regular, NavBarM3EDensity.compact],
labelBuilder: (v) => v.name,
);
final withBadges =
context.knobs.boolean(label: 'withBadges', initialValue: false);
return _buildNavBar(
context,
indicatorStyle: indicator,
size: size,
shapeFamily: shape,
density: density,
withBadges: withBadges,
);
}
@UseCase(name: 'pill_indicator', type: NavigationBarM3E)
Widget buildNavigationBarM3EPillIndicatorUseCase(BuildContext context) {
return _buildNavBar(context, indicatorStyle: NavBarM3EIndicatorStyle.pill);
}
@UseCase(name: 'underline_indicator', type: NavigationBarM3E)
Widget buildNavigationBarM3EUnderlineIndicatorUseCase(BuildContext context) {
return _buildNavBar(context,
indicatorStyle: NavBarM3EIndicatorStyle.underline);
}
@UseCase(name: 'no_indicator', type: NavigationBarM3E)
Widget buildNavigationBarM3ENoIndicatorUseCase(BuildContext context) {
return _buildNavBar(context, indicatorStyle: NavBarM3EIndicatorStyle.none);
}
@UseCase(name: 'small_size', type: NavigationBarM3E)
Widget buildNavigationBarM3ESmallSizeUseCase(BuildContext context) {
return _buildNavBar(context, size: NavBarM3ESize.small);
}
@UseCase(name: 'square_shape', type: NavigationBarM3E)
Widget buildNavigationBarM3ESquareShapeUseCase(BuildContext context) {
return _buildNavBar(context, shapeFamily: NavBarM3EShapeFamily.square);
}
@UseCase(name: 'compact_density', type: NavigationBarM3E)
Widget buildNavigationBarM3ECompactDensityUseCase(BuildContext context) {
return _buildNavBar(context, density: NavBarM3EDensity.compact);
}
@UseCase(name: 'with_badges', type: NavigationBarM3E)
Widget buildNavigationBarM3EWithBadgesUseCase(BuildContext context) {
return _buildNavBar(context, withBadges: true);
}

View file

@ -0,0 +1,35 @@
# INDEX — navigation_rail_m3e
This index summarizes Widgetbook use cases for the `navigation_rail_m3e` package.
## Components and Variants
- NavigationRailM3E
- default
- collapsed
- always_collapsed
- always_expanded
- modal
- labels_only_selected
- labels_always_hide
- three_destinations
- five_destinations_with_badges
- with_fab_slot
- with_trailing
- RailItemButtonM3E
- default
- expanded
- selected
- with_badge
- RailBadgeM3E
- default
- dot
- overflow_999+
- dense
Notes
- All use cases are placed under: `packages/navigation_rail_m3e/lib/src/widgetbook/`.
- Use cases follow plan/guide.md: `@UseCase(name: '...', type: ComponentType)` and method names `build[Component][Variant]UseCase`.
- Critical parameters are exposed via knobs; callbacks print useful messages.

View file

@ -0,0 +1,203 @@
import 'package:flutter/material.dart';
import 'package:navigation_rail_m3e/navigation_rail_m3e.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
List<NavigationRailM3ESection> _buildSections(
BuildContext context, {
required int sectionCount,
required int itemsPerSection,
required bool withBadges,
required bool useShortItems,
}) {
final icons = <IconData>[
Icons.inbox_outlined,
Icons.send_outlined,
Icons.star_outline,
Icons.archive_outlined,
Icons.delete_outline,
Icons.settings_outlined,
];
return List.generate(sectionCount, (s) {
final destinations = List.generate(itemsPerSection, (i) {
final idx = (s * itemsPerSection + i) % icons.length;
return NavigationRailM3EDestination(
icon: Icon(icons[idx]),
selectedIcon: Icon(
icons[idx].codePoint == Icons.inbox_outlined.codePoint
? Icons.inbox
: icons[idx]),
label: 'Item ${s + 1}.${i + 1}',
semanticLabel: 'Item ${s + 1}.${i + 1}',
badgeCount: withBadges
? ((i % 3 == 0)
? 0
: (i % 4 == 0)
? 1001
: (i + 1) * (s + 1))
: null,
short: useShortItems && (i % 2 == 0),
);
});
return NavigationRailM3ESection(
header: Text('Section ${s + 1}'),
destinations: destinations,
);
});
}
Widget _buildRailDemo(
BuildContext context, {
NavigationRailM3EType? forcedType,
NavigationRailM3EModality? forcedModality,
}) {
// Content knobs
final sectionsCount = context.knobs.int.slider(
label: 'sections',
initialValue: 2,
min: 1,
max: 3,
);
final itemsPerSection = context.knobs.int.slider(
label: 'items per section',
initialValue: 3,
min: 1,
max: 6,
);
final withBadges =
context.knobs.boolean(label: 'with badges', initialValue: true);
final useShortItems =
context.knobs.boolean(label: 'use short items', initialValue: false);
final sections = _buildSections(
context,
sectionCount: sectionsCount,
itemsPerSection: itemsPerSection,
withBadges: withBadges,
useShortItems: useShortItems,
);
final totalItems =
sections.fold<int>(0, (sum, s) => sum + s.destinations.length);
// Behavior knobs
final type = forcedType ??
context.knobs.object.dropdown<NavigationRailM3EType>(
label: 'type',
options: NavigationRailM3EType.values,
initialOption: NavigationRailM3EType.expanded,
labelBuilder: (v) => v.name,
);
final modality = forcedModality ??
context.knobs.object.dropdown<NavigationRailM3EModality>(
label: 'modality',
options: NavigationRailM3EModality.values,
initialOption: NavigationRailM3EModality.standard,
labelBuilder: (v) => v.name,
);
final labelBehavior =
context.knobs.object.dropdown<NavigationRailM3ELabelBehavior>(
label: 'labelBehavior',
options: NavigationRailM3ELabelBehavior.values,
initialOption: NavigationRailM3ELabelBehavior.alwaysShow,
labelBuilder: (v) => v.name,
);
final hideWhenCollapsed = context.knobs.boolean(
label: 'hideWhenCollapsed',
initialValue: false,
);
final scrollable = context.knobs.boolean(
label: 'scrollable',
initialValue: true,
);
final expandedWidth = context.knobs.double.slider(
label: 'expandedWidth',
initialValue: 280,
min: 220,
max: 360,
divisions: 14,
);
final selectedIndex = context.knobs.int.slider(
label: 'selectedIndex',
initialValue: 0,
min: 0,
max: (totalItems == 0 ? 0 : totalItems - 1),
);
// Slots knobs
final withFab = context.knobs.boolean(label: 'with FAB', initialValue: true);
final withTrailing =
context.knobs.boolean(label: 'with trailing', initialValue: false);
final trailingAtBottom =
context.knobs.boolean(label: 'trailingAtBottom', initialValue: true);
final fab = withFab
? NavigationRailM3EFabSlot(
icon: const Icon(Icons.add),
label: 'Create',
onPressed: () => print('Rail FAB pressed'),
tooltip: 'Create',
)
: null;
final trailing = withTrailing
? IconButton(
tooltip: 'Settings',
onPressed: () => print('Trailing pressed'),
icon: const Icon(Icons.settings_outlined),
)
: null;
return Row(
children: [
NavigationRailM3E(
type: type,
modality: modality,
sections: sections,
selectedIndex: selectedIndex.clamp(0, (totalItems - 1).clamp(0, 9999)),
onDestinationSelected: (i) => print('Selected index: $i'),
fab: fab,
hideWhenCollapsed: hideWhenCollapsed,
expandedWidth: expandedWidth,
onDismissModal: () => print('Dismiss modal'),
onTypeChanged: (t) => print('Type changed -> ${t.name}'),
labelBehavior: labelBehavior,
scrollable: scrollable,
trailing: trailing,
trailingAtBottom: trailingAtBottom,
),
// Fake content area to the right
Expanded(
child: Container(
height: double.infinity,
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.2),
alignment: Alignment.center,
child: const Text('Content area'),
),
)
],
);
}
@UseCase(name: 'default', type: NavigationRailM3E)
Widget buildNavigationRailM3EDefaultUseCase(BuildContext context) {
return _buildRailDemo(context);
}
@UseCase(name: 'collapsed_standard', type: NavigationRailM3E)
Widget buildNavigationRailM3ECollapsedStandardUseCase(BuildContext context) {
return _buildRailDemo(
context,
forcedType: NavigationRailM3EType.collapsed,
forcedModality: NavigationRailM3EModality.standard,
);
}
@UseCase(name: 'expanded_modal', type: NavigationRailM3E)
Widget buildNavigationRailM3EExpandedModalUseCase(BuildContext context) {
return _buildRailDemo(
context,
forcedType: NavigationRailM3EType.expanded,
forcedModality: NavigationRailM3EModality.modal,
);
}

View file

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
import 'package:navigation_rail_m3e/navigation_rail_m3e.dart';
// Note: RailBadgeM3E shows nothing when count is null; dot when 0; count otherwise.
@widgetbook.UseCase(name: 'default', type: RailBadgeM3E)
Widget buildRailBadgeM3EUseCase(BuildContext context) {
final count = context.knobs.intOrNull.slider(
label: 'count',
initialValue: 7,
min: 0,
max: 1200,
divisions: 120,
);
final maxDigits = context.knobs.int.slider(
label: 'maxDigits',
initialValue: 3,
min: 1,
max: 4,
);
final dense = context.knobs.boolean(label: 'dense', initialValue: false);
return Center(
child: RailBadgeM3E(count: count, maxDigits: maxDigits, dense: dense),
);
}
@widgetbook.UseCase(name: 'dot', type: RailBadgeM3E)
Widget buildRailBadgeM3EDotUseCase(BuildContext context) {
return const Center(child: RailBadgeM3E(count: 0));
}
@widgetbook.UseCase(name: 'overflow_999+', type: RailBadgeM3E)
Widget buildRailBadgeM3EOverflowUseCase(BuildContext context) {
return const Center(child: RailBadgeM3E(count: 1200, maxDigits: 3));
}
@widgetbook.UseCase(name: 'dense', type: RailBadgeM3E)
Widget buildRailBadgeM3EDenseUseCase(BuildContext context) {
return const Center(child: RailBadgeM3E(count: 42, dense: true));
}

View file

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:navigation_rail_m3e/navigation_rail_m3e.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
Widget _buildRailItemButtonDemo(
BuildContext context, {
required bool expanded,
}) {
final isSelected =
context.knobs.boolean(label: 'isSelected', initialValue: false);
final label = context.knobs
.string(label: 'label', initialValue: expanded ? 'Inbox' : 'Inbox');
final semantic =
context.knobs.stringOrNull(label: 'semanticLabel', initialValue: 'Inbox');
final labelBehavior =
context.knobs.object.dropdown<NavigationRailM3ELabelBehavior>(
label: 'labelBehavior',
options: NavigationRailM3ELabelBehavior.values,
initialOption: NavigationRailM3ELabelBehavior.alwaysShow,
labelBuilder: (v) => v.name,
);
final badge = context.knobs.intOrNull.slider(
label: 'badgeCount',
initialValue: null,
min: 0,
max: 200,
divisions: 20,
);
final suppressInk =
context.knobs.boolean(label: 'suppressInk', initialValue: false);
final useAltIcon =
context.knobs.boolean(label: 'useAltIcon', initialValue: false);
final icon = Icon(useAltIcon ? Icons.star : Icons.inbox_outlined);
final selectedIcon = Icon(useAltIcon ? Icons.star : Icons.inbox);
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.withValues(alpha: 0.3)),
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(8),
child: RailItemButtonM3E(
icon: icon,
selectedIcon: selectedIcon,
isSelected: isSelected,
onPressed: () => print('RailItemButtonM3E pressed'),
expanded: expanded,
labelBehavior: labelBehavior,
label: label,
semanticLabel: semantic,
suppressInk: suppressInk,
badgeCount: badge,
),
),
),
),
);
}
@UseCase(name: 'collapsed', type: RailItemButtonM3E)
Widget buildRailItemButtonM3ECollapsedUseCase(BuildContext context) {
return _buildRailItemButtonDemo(context, expanded: false);
}
@UseCase(name: 'expanded', type: RailItemButtonM3E)
Widget buildRailItemButtonM3EExpandedUseCase(BuildContext context) {
return _buildRailItemButtonDemo(context, expanded: true);
}

View file

@ -0,0 +1,29 @@
# Widgetbook Index — progress_indicator_m3e
This index lists all components in the `progress_indicator_m3e` package and their Widgetbook variants.
## Components and Variants
- CircularProgressIndicatorM3E
- indeterminate
- determinate
- flat
- wavy
- size_s
- size_m
- LinearProgressIndicatorM3E
- indeterminate
- determinate
- size_s
- size_m
- ProgressWithLabelM3E
- default
- determinate_with_label
- long_label_text
Notes
- Knobs are provided for critical params (progress/value, size, shape, rotation/phase/inset as applicable).
- Themes are provided globally by the Widgetbook app (no local Theme wrapping).
- Complex parameters such as custom colors are left at sensible defaults (see TODOs in code).

View file

@ -0,0 +1,204 @@
import 'package:flutter/material.dart';
import 'package:progress_indicator_m3e/progress_indicator_m3e.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
// Use cases for CircularProgressIndicatorM3E per plan/guide.md
// - Themes are injected globally by the Widgetbook app.
// - Knobs for value (when determinate), size, shape, rotation.
// - TODO: Consider adding color knobs for active/track colors.
@UseCase(name: 'indeterminate', type: CircularProgressIndicatorM3E)
Widget buildCircularProgressIndicatorM3EIndeterminateUseCase(
BuildContext context) {
final CircularProgressM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: CircularProgressM3ESize.m,
options: CircularProgressM3ESize.values,
labelBuilder: (v) => v.name,
);
final ProgressM3EShape shape = context.knobs.object.dropdown(
label: 'shape',
initialOption: ProgressM3EShape.wavy,
options: ProgressM3EShape.values,
labelBuilder: (v) => v.name,
);
final double rotation = context.knobs.double.slider(
label: 'rotation (rad)',
initialValue: 0.0,
min: 0.0,
max: 6.28318,
divisions: 36,
);
return Center(
child: CircularProgressIndicatorM3E(
value: null, // indeterminate
size: size,
shape: shape,
rotation: rotation,
),
);
}
@UseCase(name: 'determinate', type: CircularProgressIndicatorM3E)
Widget buildCircularProgressIndicatorM3EDeterminateUseCase(
BuildContext context) {
final double value = context.knobs.double.slider(
label: 'value',
initialValue: 0.6,
min: 0.0,
max: 1.0,
divisions: 100,
);
final CircularProgressM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: CircularProgressM3ESize.m,
options: CircularProgressM3ESize.values,
labelBuilder: (v) => v.name,
);
final ProgressM3EShape shape = context.knobs.object.dropdown(
label: 'shape',
initialOption: ProgressM3EShape.wavy,
options: ProgressM3EShape.values,
labelBuilder: (v) => v.name,
);
return Center(
child: CircularProgressIndicatorM3E(
value: value,
size: size,
shape: shape,
),
);
}
@UseCase(name: 'flat', type: CircularProgressIndicatorM3E)
Widget buildCircularProgressIndicatorM3EFlatUseCase(BuildContext context) {
final double? valueOrNull = context.knobs.boolean(
label: 'determinate',
initialValue: true,
)
? context.knobs.double.slider(
label: 'value',
initialValue: 0.75,
min: 0.0,
max: 1.0,
divisions: 100,
)
: null;
final CircularProgressM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: CircularProgressM3ESize.m,
options: CircularProgressM3ESize.values,
labelBuilder: (v) => v.name,
);
return Center(
child: CircularProgressIndicatorM3E(
value: valueOrNull,
size: size,
shape: ProgressM3EShape.flat,
),
);
}
@UseCase(name: 'wavy', type: CircularProgressIndicatorM3E)
Widget buildCircularProgressIndicatorM3EWavyUseCase(BuildContext context) {
final double? valueOrNull = context.knobs.boolean(
label: 'determinate',
initialValue: false,
)
? context.knobs.double.slider(
label: 'value',
initialValue: 0.4,
min: 0.0,
max: 1.0,
divisions: 100,
)
: null;
final CircularProgressM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: CircularProgressM3ESize.m,
options: CircularProgressM3ESize.values,
labelBuilder: (v) => v.name,
);
final double rotation = context.knobs.double.slider(
label: 'rotation (rad)',
initialValue: 0.0,
min: 0.0,
max: 6.28318,
divisions: 36,
);
return Center(
child: CircularProgressIndicatorM3E(
value: valueOrNull,
size: size,
shape: ProgressM3EShape.wavy,
rotation: rotation,
),
);
}
@UseCase(name: 'size_s', type: CircularProgressIndicatorM3E)
Widget buildCircularProgressIndicatorM3ESizeSUseCase(BuildContext context) {
final ProgressM3EShape shape = context.knobs.object.dropdown(
label: 'shape',
initialOption: ProgressM3EShape.wavy,
options: ProgressM3EShape.values,
labelBuilder: (v) => v.name,
);
final double? valueOrNull = context.knobs.boolean(
label: 'determinate',
initialValue: true,
)
? context.knobs.double.slider(
label: 'value',
initialValue: 1.0,
min: 0.0,
max: 1.0,
divisions: 100,
)
: null;
return Center(
child: CircularProgressIndicatorM3E(
value: valueOrNull,
size: CircularProgressM3ESize.s,
shape: shape,
),
);
}
@UseCase(name: 'size_m', type: CircularProgressIndicatorM3E)
Widget buildCircularProgressIndicatorM3ESizeMUseCase(BuildContext context) {
final ProgressM3EShape shape = context.knobs.object.dropdown(
label: 'shape',
initialOption: ProgressM3EShape.wavy,
options: ProgressM3EShape.values,
labelBuilder: (v) => v.name,
);
final double? valueOrNull = context.knobs.boolean(
label: 'determinate',
initialValue: true,
)
? context.knobs.double.slider(
label: 'value',
initialValue: 0.0, // boundary case
min: 0.0,
max: 1.0,
divisions: 100,
)
: null;
return Center(
child: CircularProgressIndicatorM3E(
value: valueOrNull,
size: CircularProgressM3ESize.m,
shape: shape,
),
);
}

View file

@ -0,0 +1,188 @@
import 'package:flutter/material.dart';
import 'package:progress_indicator_m3e/progress_indicator_m3e.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
// Use cases for LinearProgressIndicatorM3E per plan/guide.md
// - Themes are injected globally by the Widgetbook app.
// - Knobs for value (when determinate), size, shape, phase (wavy), and inset.
// - TODO: Consider adding color knobs for active/track colors.
@UseCase(name: 'indeterminate', type: LinearProgressIndicatorM3E)
Widget buildLinearProgressIndicatorM3EIndeterminateUseCase(
BuildContext context) {
final LinearProgressM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: LinearProgressM3ESize.m,
options: LinearProgressM3ESize.values,
labelBuilder: (v) => v.name,
);
final ProgressM3EShape shape = context.knobs.object.dropdown(
label: 'shape',
initialOption: ProgressM3EShape.wavy,
options: ProgressM3EShape.values,
labelBuilder: (v) => v.name,
);
final double phase = context.knobs.double.slider(
label: 'phase (rad) — wavy only',
initialValue: 0.0,
min: 0.0,
max: 6.28318,
divisions: 36,
);
final double inset = context.knobs.double.slider(
label: 'inset',
initialValue: 4.0,
min: 0.0,
max: 24.0,
divisions: 24,
);
return Padding(
padding: const EdgeInsets.all(16),
child: LinearProgressIndicatorM3E(
value: null, // indeterminate
size: size,
shape: shape,
phase: phase,
inset: inset,
),
);
}
@UseCase(name: 'determinate', type: LinearProgressIndicatorM3E)
Widget buildLinearProgressIndicatorM3EDeterminateUseCase(
BuildContext context) {
final double value = context.knobs.double.slider(
label: 'value',
initialValue: 0.5,
min: 0.0,
max: 1.0,
divisions: 100,
);
final LinearProgressM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: LinearProgressM3ESize.m,
options: LinearProgressM3ESize.values,
labelBuilder: (v) => v.name,
);
final ProgressM3EShape shape = context.knobs.object.dropdown(
label: 'shape',
initialOption: ProgressM3EShape.wavy,
options: ProgressM3EShape.values,
labelBuilder: (v) => v.name,
);
final double phase = context.knobs.double.slider(
label: 'phase (rad) — wavy only',
initialValue: 0.0,
min: 0.0,
max: 6.28318,
divisions: 36,
);
final double inset = context.knobs.double.slider(
label: 'inset',
initialValue: 4.0,
min: 0.0,
max: 24.0,
divisions: 24,
);
return Padding(
padding: const EdgeInsets.all(16),
child: LinearProgressIndicatorM3E(
value: value,
size: size,
shape: shape,
phase: phase,
inset: inset,
),
);
}
@UseCase(name: 'size_s', type: LinearProgressIndicatorM3E)
Widget buildLinearProgressIndicatorM3ESizeSUseCase(BuildContext context) {
final double? valueOrNull = context.knobs.boolean(
label: 'determinate',
initialValue: true,
)
? context.knobs.double.slider(
label: 'value',
initialValue: 0.0, // boundary case
min: 0.0,
max: 1.0,
divisions: 100,
)
: null;
final ProgressM3EShape shape = context.knobs.object.dropdown(
label: 'shape',
initialOption: ProgressM3EShape.flat,
options: ProgressM3EShape.values,
labelBuilder: (v) => v.name,
);
final double inset = context.knobs.double.slider(
label: 'inset',
initialValue: 0.0,
min: 0.0,
max: 24.0,
divisions: 24,
);
return Padding(
padding: const EdgeInsets.all(16),
child: LinearProgressIndicatorM3E(
value: valueOrNull,
size: LinearProgressM3ESize.s,
shape: shape,
inset: inset,
),
);
}
@UseCase(name: 'size_m', type: LinearProgressIndicatorM3E)
Widget buildLinearProgressIndicatorM3ESizeMUseCase(BuildContext context) {
final double? valueOrNull = context.knobs.boolean(
label: 'determinate',
initialValue: true,
)
? context.knobs.double.slider(
label: 'value',
initialValue: 1.0, // boundary case
min: 0.0,
max: 1.0,
divisions: 100,
)
: null;
final ProgressM3EShape shape = context.knobs.object.dropdown(
label: 'shape',
initialOption: ProgressM3EShape.wavy,
options: ProgressM3EShape.values,
labelBuilder: (v) => v.name,
);
final double phase = context.knobs.double.slider(
label: 'phase (rad) — wavy only',
initialValue: 0.0,
min: 0.0,
max: 6.28318,
divisions: 36,
);
final double inset = context.knobs.double.slider(
label: 'inset',
initialValue: 8.0,
min: 0.0,
max: 24.0,
divisions: 24,
);
return Padding(
padding: const EdgeInsets.all(16),
child: LinearProgressIndicatorM3E(
value: valueOrNull,
size: LinearProgressM3ESize.m,
shape: shape,
phase: phase,
inset: inset,
),
);
}

View file

@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:progress_indicator_m3e/progress_indicator_m3e.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
// Use cases for ProgressWithLabelM3E per plan/guide.md
// - Themes are injected globally by the Widgetbook app.
// - Knobs for value, size, and label style/text.
@UseCase(name: 'default', type: ProgressWithLabelM3E)
Widget buildProgressWithLabelM3EDefaultUseCase(BuildContext context) {
final double value = context.knobs.double.slider(
label: 'value',
initialValue: 0.6,
min: 0.0,
max: 1.0,
divisions: 100,
);
final CircularProgressM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: CircularProgressM3ESize.m,
options: CircularProgressM3ESize.values,
labelBuilder: (v) => v.name,
);
return Center(
child: ProgressWithLabelM3E(
value: value,
size: size,
),
);
}
@UseCase(name: 'determinate_with_label', type: ProgressWithLabelM3E)
Widget buildProgressWithLabelM3EDeterminateWithLabelUseCase(
BuildContext context) {
final double value = context.knobs.double.slider(
label: 'value',
initialValue: 0.85,
min: 0.0,
max: 1.0,
divisions: 100,
);
final CircularProgressM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: CircularProgressM3ESize.m,
options: CircularProgressM3ESize.values,
labelBuilder: (v) => v.name,
);
final double fontSize = context.knobs.double.slider(
label: 'fontSize',
initialValue: 12.0,
min: 8.0,
max: 32.0,
divisions: 24,
);
return Center(
child: ProgressWithLabelM3E(
value: value,
size: size,
textStyle: TextStyle(fontSize: fontSize),
),
);
}
@UseCase(name: 'long_label_text', type: ProgressWithLabelM3E)
Widget buildProgressWithLabelM3ELongLabelTextUseCase(BuildContext context) {
final double value = context.knobs.double.slider(
label: 'value',
initialValue: 0.33,
min: 0.0,
max: 1.0,
divisions: 100,
);
final CircularProgressM3ESize size = context.knobs.object.dropdown(
label: 'size',
initialOption: CircularProgressM3ESize.m,
options: CircularProgressM3ESize.values,
labelBuilder: (v) => v.name,
);
final String label = context.knobs.string(
label: 'label (overrides %)',
initialValue:
'Downloading super-duper long file name with many-many characters... 33%',
);
final bool useCustom = context.knobs.boolean(
label: 'use custom text instead of percent',
initialValue: true,
);
// Note: The stock ProgressWithLabelM3E renders percentage by default.
// To show a long label, we overlay an extra Text widget for demonstration.
return SizedBox(
width: size.diameterWavy,
height: size.diameterWavy,
child: Stack(
alignment: Alignment.center,
children: [
CircularProgressIndicatorM3E(value: value, size: size),
if (useCustom)
Padding(
padding: const EdgeInsets.all(4.0),
child: Text(
label,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.labelSmall,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
)
else
ProgressWithLabelM3E(value: value, size: size),
],
),
);
}

View file

@ -0,0 +1,27 @@
# INDEX — slider_m3e
This index lists all Widgetbook use cases generated for the slider_m3e package.
## Components and Variants
- SliderM3E
- default
- discrete
- disabled
- extremes_0_to_100
- negative_range
- min_equals_max
- RangeSliderM3E
- default
- discrete
- disabled
- extremes_0_to_100
- negative_range
- min_equals_max
Notes
- All variants follow plan/guide.md rules with @UseCase annotations and required method signatures.
- Knobs are provided for critical parameters (values, min, max, divisions) and key visuals (size, emphasis, shapeFamily, density, value indicator).
- Callbacks print helpful messages to the console.
- Range labels are included where useful; complex configurations are kept simple with TODOs where appropriate.

View file

@ -0,0 +1,191 @@
import 'package:flutter/material.dart';
import 'package:slider_m3e/slider_m3e.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
Widget _buildRangeSliderM3EDemo(
BuildContext context, {
double? forcedMin,
double? forcedMax,
int? forcedDivisions,
bool disabled = false,
bool zeroRange = false,
}) {
// Critical params via knobs
final minKnob = forcedMin ??
context.knobs.double.slider(
label: 'min',
initialValue: 0,
min: -100,
max: 100,
divisions: 40,
);
final maxKnob = forcedMax ??
context.knobs.double.slider(
label: 'max',
initialValue: 100,
min: -100,
max: 200,
divisions: 60,
);
double min = minKnob;
double max = maxKnob;
// Force min == max when zeroRange is requested
if (zeroRange) {
final fixed = context.knobs.double.slider(
label: 'fixed value (min==max)',
initialValue: 25,
min: -100,
max: 100,
divisions: 40,
);
min = fixed;
max = fixed;
}
if (min >= max) {
// Ensure valid range
max = min + 1;
}
// Pick start/end values within [min, max]
final start = context.knobs.double.slider(
label: 'start',
initialValue: min + (max - min) * 0.25,
min: min,
max: max,
divisions: 100,
);
final end = context.knobs.double.slider(
label: 'end',
initialValue: min + (max - min) * 0.75,
min: min,
max: max,
divisions: 100,
);
final divisions = forcedDivisions ??
context.knobs.intOrNull.slider(
label: 'divisions',
initialValue: null,
min: 1,
max: 20,
divisions: 19,
);
// Visual params via knobs
final size = context.knobs.object.dropdown<SliderM3ESize>(
label: 'size',
initialOption: SliderM3ESize.medium,
options: SliderM3ESize.values,
labelBuilder: (v) => v.name,
);
final emphasis = context.knobs.object.dropdown<SliderM3EEmphasis>(
label: 'emphasis',
initialOption: SliderM3EEmphasis.primary,
options: SliderM3EEmphasis.values,
labelBuilder: (v) => v.name,
);
final shape = context.knobs.object.dropdown<SliderM3EShapeFamily>(
label: 'shapeFamily',
initialOption: SliderM3EShapeFamily.round,
options: SliderM3EShapeFamily.values,
labelBuilder: (v) => v.name,
);
final density = context.knobs.object.dropdown<SliderM3EDensity>(
label: 'density',
initialOption: SliderM3EDensity.regular,
options: SliderM3EDensity.values,
labelBuilder: (v) => v.name,
);
final showValueIndicator = context.knobs.boolean(
label: 'showValueIndicator',
initialValue: false,
);
// Labels & semantics
final hasLabels = context.knobs.boolean(label: 'use RangeLabels', initialValue: false);
final startLabel = hasLabels
? context.knobs.string(label: 'start label', initialValue: 'Start')
: null;
final endLabel = hasLabels
? context.knobs.string(label: 'end label', initialValue: 'End')
: null;
final labels = hasLabels && startLabel != null && endLabel != null
? RangeLabels(startLabel, endLabel)
: null;
final semanticLabel = zeroRange
? null
: context.knobs.stringOrNull(
label: 'semanticLabel',
initialValue: 'Progress',
);
final onChanged = disabled
? null
: (RangeValues v) => print('RangeSliderM3E onChanged -> ${v.start} - ${v.end}');
final onChangeStart = disabled
? null
: (RangeValues v) => print('RangeSliderM3E onChangeStart -> ${v.start} - ${v.end}');
final onChangeEnd = disabled
? null
: (RangeValues v) => print('RangeSliderM3E onChangeEnd -> ${v.start} - ${v.end}');
return Padding(
padding: const EdgeInsets.all(16),
child: RangeSliderM3E(
values: RangeValues(
start.clamp(min, max),
end.clamp(min, max),
),
onChanged: onChanged,
onChangeStart: onChangeStart,
onChangeEnd: onChangeEnd,
min: min,
max: max,
divisions: divisions,
labels: labels,
semanticLabel: semanticLabel,
size: size,
emphasis: emphasis,
shapeFamily: shape,
density: density,
showValueIndicator: showValueIndicator,
),
);
}
@UseCase(name: 'default', type: RangeSliderM3E)
Widget buildRangeSliderM3EDefaultUseCase(BuildContext context) {
return _buildRangeSliderM3EDemo(context);
}
@UseCase(name: 'discrete', type: RangeSliderM3E)
Widget buildRangeSliderM3EDiscreteUseCase(BuildContext context) {
return _buildRangeSliderM3EDemo(context, forcedDivisions: 6);
}
@UseCase(name: 'disabled', type: RangeSliderM3E)
Widget buildRangeSliderM3EDisabledUseCase(BuildContext context) {
return _buildRangeSliderM3EDemo(context, disabled: true);
}
@UseCase(name: 'extremes_0_to_100', type: RangeSliderM3E)
Widget buildRangeSliderM3EExtremes0100UseCase(BuildContext context) {
return _buildRangeSliderM3EDemo(context, forcedMin: 0, forcedMax: 100);
}
@UseCase(name: 'negative_range', type: RangeSliderM3E)
Widget buildRangeSliderM3ENegativeRangeUseCase(BuildContext context) {
return _buildRangeSliderM3EDemo(context, forcedMin: -100, forcedMax: 0);
}
@UseCase(name: 'min_equals_max', type: RangeSliderM3E)
Widget buildRangeSliderM3EMinEqualsMaxUseCase(BuildContext context) {
// Zero-range; interactions disabled to avoid semantic division by zero
return _buildRangeSliderM3EDemo(context, zeroRange: true, disabled: true);
}

View file

@ -0,0 +1,186 @@
import 'package:flutter/material.dart';
import 'package:slider_m3e/slider_m3e.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
Widget _buildSliderM3EDemo(
BuildContext context, {
double? forcedMin,
double? forcedMax,
int? forcedDivisions,
bool disabled = false,
bool zeroRange = false,
}) {
// Critical params via knobs
final minKnob = forcedMin ??
context.knobs.double.slider(
label: 'min',
initialValue: 0,
min: -100,
max: 100,
divisions: 40,
);
final maxKnob = forcedMax ??
context.knobs.double.slider(
label: 'max',
initialValue: 100,
min: -100,
max: 200,
divisions: 60,
);
double min = minKnob;
double max = maxKnob;
// Special handling when zeroRange is requested
if (zeroRange) {
// Force min == max and disable interactions to avoid semantic division by zero
final fixed = context.knobs.double.slider(
label: 'fixed value (min==max)',
initialValue: 50,
min: -100,
max: 100,
divisions: 40,
);
min = fixed;
max = fixed;
}
if (min >= max) {
// Guard against invalid ranges; ensure there is at least a small span
max = min + 1;
}
final value = context.knobs.double.slider(
label: 'value',
initialValue: (min + max) / 2,
min: min,
max: max,
divisions: 100,
);
final divisions = forcedDivisions ??
context.knobs.intOrNull.slider(
label: 'divisions',
initialValue: null,
min: 1,
max: 20,
divisions: 19,
);
// Visual params via knobs
final size = context.knobs.object.dropdown<SliderM3ESize>(
label: 'size',
initialOption: SliderM3ESize.medium,
options: SliderM3ESize.values,
labelBuilder: (v) => v.name,
);
final emphasis = context.knobs.object.dropdown<SliderM3EEmphasis>(
label: 'emphasis',
initialOption: SliderM3EEmphasis.primary,
options: SliderM3EEmphasis.values,
labelBuilder: (v) => v.name,
);
final shape = context.knobs.object.dropdown<SliderM3EShapeFamily>(
label: 'shapeFamily',
initialOption: SliderM3EShapeFamily.round,
options: SliderM3EShapeFamily.values,
labelBuilder: (v) => v.name,
);
final density = context.knobs.object.dropdown<SliderM3EDensity>(
label: 'density',
initialOption: SliderM3EDensity.regular,
options: SliderM3EDensity.values,
labelBuilder: (v) => v.name,
);
final showValueIndicator = context.knobs.boolean(
label: 'showValueIndicator',
initialValue: false,
);
final withStartIcon = context.knobs.boolean(
label: 'startIcon',
initialValue: false,
);
final withEndIcon = context.knobs.boolean(
label: 'endIcon',
initialValue: false,
);
// Content params
final label = context.knobs.stringOrNull(
label: 'label',
initialValue: null,
);
// Avoid semantic callback when zeroRange to prevent divide-by-zero in tokens formatting
final semanticLabel = zeroRange
? null
: context.knobs.stringOrNull(
label: 'semanticLabel',
initialValue: 'Progress',
);
final onChanged = disabled
? null
: (double v) => print('SliderM3E onChanged -> $v');
final onChangeStart = disabled
? null
: (double v) => print('SliderM3E onChangeStart -> $v');
final onChangeEnd = disabled
? null
: (double v) => print('SliderM3E onChangeEnd -> $v');
return Padding(
padding: const EdgeInsets.all(16),
child: SliderM3E(
value: value,
onChanged: onChanged,
onChangeStart: onChangeStart,
onChangeEnd: onChangeEnd,
min: min,
max: max,
divisions: divisions,
label: label,
semanticLabel: semanticLabel,
size: size,
emphasis: emphasis,
shapeFamily: shape,
density: density,
showValueIndicator: showValueIndicator,
startIcon: withStartIcon ? const Icon(Icons.remove) : null,
endIcon: withEndIcon ? const Icon(Icons.add) : null,
),
);
}
@UseCase(name: 'default', type: SliderM3E)
Widget buildSliderM3EDefaultUseCase(BuildContext context) {
return _buildSliderM3EDemo(context);
}
@UseCase(name: 'discrete', type: SliderM3E)
Widget buildSliderM3EDiscreteUseCase(BuildContext context) {
// Provide a default divisions count; still knob-customizable via 'divisions'
return _buildSliderM3EDemo(context, forcedDivisions: 5);
}
@UseCase(name: 'disabled', type: SliderM3E)
Widget buildSliderM3EDisabledUseCase(BuildContext context) {
return _buildSliderM3EDemo(context, disabled: true);
}
@UseCase(name: 'extremes_0_to_100', type: SliderM3E)
Widget buildSliderM3EExtremes0100UseCase(BuildContext context) {
return _buildSliderM3EDemo(context, forcedMin: 0, forcedMax: 100);
}
@UseCase(name: 'negative_range', type: SliderM3E)
Widget buildSliderM3ENegativeRangeUseCase(BuildContext context) {
return _buildSliderM3EDemo(context, forcedMin: -100, forcedMax: 0);
}
@UseCase(name: 'min_equals_max', type: SliderM3E)
Widget buildSliderM3EMinEqualsMaxUseCase(BuildContext context) {
// Show a zero-range slider (min == max), interactions disabled to avoid semantic math
return _buildSliderM3EDemo(context, zeroRange: true, disabled: true);
}

View file

@ -0,0 +1,31 @@
# Widgetbook Index — split_button_m3e
This index summarizes all Widgetbook use cases defined for SplitButtonM3E.
Component: SplitButtonM3E<T>
Variants
- default
- filled
- tonal
- elevated
- outlined
- text
- xs
- sm
- md
- lg
- xl
- round
- square
- with_icon
- with_label
- long_label
- many_items
- empty_items
- disabled
Notes
- Knobs provided for critical parameters: items count, selection (via choosing menu item), emphasis, size, shape, trailingAlignment, enabled, tooltips, and content (label/icon).
- Callbacks print informative messages to the console when the primary action is pressed or a menu item is selected.
- Complex objects use sensible defaults; see TODO comments for potential icon picker enhancement.

View file

@ -0,0 +1,272 @@
import 'package:flutter/material.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
import 'package:split_button_m3e/split_button_m3e.dart';
Widget _buildSplitButtonDemo(
BuildContext context, {
SplitButtonM3EEmphasis? emphasisFixed,
SplitButtonM3ESize? sizeFixed,
SplitButtonM3EShape? shapeFixed,
bool? enabledFixed,
String? labelFixed,
IconData? iconFixed,
int? itemsFixedCount,
}) {
final emphasis = emphasisFixed ??
context.knobs.object.dropdown<SplitButtonM3EEmphasis>(
label: 'emphasis',
initialOption: SplitButtonM3EEmphasis.filled,
options: SplitButtonM3EEmphasis.values,
labelBuilder: (v) => v.name,
);
final size = sizeFixed ?? context.knobs.object.dropdown<SplitButtonM3ESize>(
label: 'size',
initialOption: SplitButtonM3ESize.md,
options: SplitButtonM3ESize.values,
labelBuilder: (v) => v.name,
);
final shape = shapeFixed ??
context.knobs.object.dropdown<SplitButtonM3EShape>(
label: 'shape',
initialOption: SplitButtonM3EShape.round,
options: SplitButtonM3EShape.values,
labelBuilder: (v) => v.name,
);
final trailingAlignment = context.knobs.object
.dropdown<SplitButtonM3ETrailingAlignment>(
label: 'trailingAlignment',
initialOption: SplitButtonM3ETrailingAlignment.opticalCenter,
options: SplitButtonM3ETrailingAlignment.values,
labelBuilder: (v) => v.name,
);
final enabled = enabledFixed ??
context.knobs.boolean(label: 'enabled', initialValue: true);
// Content knobs
final effectiveLabel = labelFixed ??
context.knobs.string(label: 'label', initialValue: 'Save');
final includeIcon = iconFixed != null
? true
: context.knobs.boolean(label: 'leadingIcon', initialValue: true);
final effectiveIcon = iconFixed ?? Icons.save_outlined; // TODO: icon picker knob
// Tooltips knobs (helpful for semantics and tests)
final leadingTooltip = context.knobs
.string(label: 'leadingTooltip', initialValue: 'Save');
final trailingTooltip = context.knobs
.string(label: 'trailingTooltip', initialValue: 'Open menu');
// Items knobs
final itemCount = itemsFixedCount ?? context.knobs.int.slider(
label: 'items count',
initialValue: 4,
min: 0,
max: 120,
divisions: 120,
);
final disableEveryNth = context.knobs.int.slider(
label: 'disable every Nth (0 = none)',
initialValue: 0,
min: 0,
max: 10,
divisions: 10,
);
final items = List.generate(itemCount, (i) {
final disabled = disableEveryNth == 0 ? false : (i % disableEveryNth == 0);
return SplitButtonM3EItem<String>(
value: 'value_$i',
child: 'Option $i',
enabled: !disabled,
);
});
return Center(
child: SplitButtonM3E<String>(
size: size,
shape: shape,
emphasis: emphasis,
label: effectiveLabel.isEmpty ? null : effectiveLabel,
leadingIcon: includeIcon ? effectiveIcon : null,
onPressed: enabled
? () => print(
'Primary pressed: size=${size.name}, shape=${shape.name}, emphasis=${emphasis.name}',
)
: null,
items: items,
onSelected: (v) => print('Menu selected → $v'),
trailingAlignment: trailingAlignment,
leadingTooltip: leadingTooltip.isEmpty ? null : leadingTooltip,
trailingTooltip: trailingTooltip.isEmpty ? null : trailingTooltip,
enabled: enabled,
),
);
}
// Default
@widgetbook.UseCase(name: 'default', type: SplitButtonM3E)
Widget buildSplitButtonM3EDefaultUseCase(BuildContext context) {
return _buildSplitButtonDemo(context);
}
// Emphasis variants
@widgetbook.UseCase(name: 'filled', type: SplitButtonM3E)
Widget buildSplitButtonM3EFilledUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
emphasisFixed: SplitButtonM3EEmphasis.filled,
);
}
@widgetbook.UseCase(name: 'tonal', type: SplitButtonM3E)
Widget buildSplitButtonM3ETonalUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
emphasisFixed: SplitButtonM3EEmphasis.tonal,
);
}
@widgetbook.UseCase(name: 'elevated', type: SplitButtonM3E)
Widget buildSplitButtonM3EElevatedUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
emphasisFixed: SplitButtonM3EEmphasis.elevated,
);
}
@widgetbook.UseCase(name: 'outlined', type: SplitButtonM3E)
Widget buildSplitButtonM3EOutlinedUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
emphasisFixed: SplitButtonM3EEmphasis.outlined,
);
}
@widgetbook.UseCase(name: 'text', type: SplitButtonM3E)
Widget buildSplitButtonM3ETextUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
emphasisFixed: SplitButtonM3EEmphasis.text,
);
}
// Size variants
@widgetbook.UseCase(name: 'xs', type: SplitButtonM3E)
Widget buildSplitButtonM3EXSUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
sizeFixed: SplitButtonM3ESize.xs,
);
}
@widgetbook.UseCase(name: 'sm', type: SplitButtonM3E)
Widget buildSplitButtonM3ESMUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
sizeFixed: SplitButtonM3ESize.sm,
);
}
@widgetbook.UseCase(name: 'md', type: SplitButtonM3E)
Widget buildSplitButtonM3EMDUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
sizeFixed: SplitButtonM3ESize.md,
);
}
@widgetbook.UseCase(name: 'lg', type: SplitButtonM3E)
Widget buildSplitButtonM3ELGUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
sizeFixed: SplitButtonM3ESize.lg,
);
}
@widgetbook.UseCase(name: 'xl', type: SplitButtonM3E)
Widget buildSplitButtonM3EXLUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
sizeFixed: SplitButtonM3ESize.xl,
);
}
// Shape variants
@widgetbook.UseCase(name: 'round', type: SplitButtonM3E)
Widget buildSplitButtonM3ERoundUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
shapeFixed: SplitButtonM3EShape.round,
);
}
@widgetbook.UseCase(name: 'square', type: SplitButtonM3E)
Widget buildSplitButtonM3ESquareUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
shapeFixed: SplitButtonM3EShape.square,
);
}
// Content variants
@widgetbook.UseCase(name: 'with_icon', type: SplitButtonM3E)
Widget buildSplitButtonM3EWithIconUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
labelFixed: '',
iconFixed: Icons.save_outlined,
);
}
@widgetbook.UseCase(name: 'with_label', type: SplitButtonM3E)
Widget buildSplitButtonM3EWithLabelUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
labelFixed: 'Save',
iconFixed: null,
);
}
@widgetbook.UseCase(name: 'long_label', type: SplitButtonM3E)
Widget buildSplitButtonM3ELongLabelUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
labelFixed: 'Save document and sync to cloud',
iconFixed: Icons.cloud_upload_outlined,
);
}
// Edge cases
@widgetbook.UseCase(name: 'many_items', type: SplitButtonM3E)
Widget buildSplitButtonM3EManyItemsUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
itemsFixedCount: 60,
);
}
@widgetbook.UseCase(name: 'empty_items', type: SplitButtonM3E)
Widget buildSplitButtonM3EEmptyItemsUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
itemsFixedCount: 0,
);
}
@widgetbook.UseCase(name: 'disabled', type: SplitButtonM3E)
Widget buildSplitButtonM3EDisabledUseCase(BuildContext context) {
return _buildSplitButtonDemo(
context,
enabledFixed: false,
);
}

View file

@ -0,0 +1,43 @@
# Widgetbook Index — toolbar_m3e
This file summarizes the Widgetbook use cases for the `toolbar_m3e` package.
Component inventory:
- ToolbarM3E — primary toolbar component
- ToolbarIconButtonM3E — wrapper for toolbar action icon buttons
- ToolbarM3EWidget — internal placeholder/demo (not exported); no use cases generated
## ToolbarM3E
Variants implemented:
- default
- surface
- tonal
- primary
- small
- medium
- large
- compact
- regular
- with_leading
- with_trailing_actions
- with_overflow
- long_title
- centered_title
Notes:
- Critical parameters (variant, size, density, shapeFamily, titles, action count) are exposed via knobs.
- Callbacks print informative messages to console.
- Theme is assumed to be injected globally by the host app.
## ToolbarIconButtonM3E
Variants implemented:
- default
- destructive
- custom_color_and_size
Notes:
- Knobs include enabled, iconSize, and color where applicable.
- Destructive variant showcases error-colored action.
## ToolbarM3EWidget
- Not exported by the package; treated as internal. Skipped for widgetbook use cases.

View file

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
import 'package:toolbar_m3e/toolbar_m3e.dart';
@UseCase(name: 'default', type: ToolbarIconButtonM3E)
Widget buildToolbarIconButtonM3EDefaultUseCase(BuildContext context) {
final enabled = context.knobs.boolean(label: 'enabled', initialValue: true);
final iconSize = context.knobs.double.slider(
label: 'iconSize',
initialValue: 24,
min: 12,
max: 48,
divisions: 12,
);
final color = context.knobs.colorOrNull(label: 'color', initialValue: null);
return Center(
child: ToolbarIconButtonM3E(
action: ToolbarActionM3E(
icon: Icons.search,
tooltip: 'Search',
label: 'Search',
enabled: enabled,
onPressed: () => print('ToolbarIconButtonM3E -> Search pressed'),
),
iconSize: iconSize,
color: color,
),
);
}
@UseCase(name: 'destructive', type: ToolbarIconButtonM3E)
Widget buildToolbarIconButtonM3EDestructiveUseCase(BuildContext context) {
final enabled = context.knobs.boolean(label: 'enabled', initialValue: true);
return Center(
child: ToolbarIconButtonM3E(
action: ToolbarActionM3E(
icon: Icons.delete,
tooltip: 'Delete',
label: 'Delete',
isDestructive: true,
enabled: enabled,
onPressed: () => print('ToolbarIconButtonM3E -> Delete pressed'),
),
color: Theme.of(context).colorScheme.error,
),
);
}
@UseCase(name: 'custom_color_and_size', type: ToolbarIconButtonM3E)
Widget buildToolbarIconButtonM3ECustomStyleUseCase(BuildContext context) {
final color = context.knobs.color(label: 'color', initialValue: Colors.teal);
final iconSize = context.knobs.double.slider(
label: 'iconSize',
initialValue: 32,
min: 16,
max: 56,
);
return Center(
child: ToolbarIconButtonM3E(
action: ToolbarActionM3E(
icon: Icons.share,
tooltip: 'Share',
label: 'Share',
onPressed: () => print('ToolbarIconButtonM3E -> Share pressed'),
),
color: color,
iconSize: iconSize,
),
);
}

View file

@ -0,0 +1,192 @@
import 'package:flutter/material.dart';
import 'package:toolbar_m3e/toolbar_m3e.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
// Helper to build a ToolbarM3E with comprehensive knobs per guide
Widget _buildToolbarDemo(
BuildContext context, {
ToolbarM3EVariant? forcedVariant,
ToolbarM3ESize? forcedSize,
ToolbarM3EDensity? forcedDensity,
String? forcedTitle,
bool withLeading = false,
int? forcedActionCount,
bool longTitle = false,
bool centerTitle = false,
}) {
final variant = forcedVariant ??
context.knobs.object.dropdown<ToolbarM3EVariant>(
label: 'variant',
initialOption: ToolbarM3EVariant.surface,
options: ToolbarM3EVariant.values,
labelBuilder: (v) => v.name,
);
final size = forcedSize ??
context.knobs.object.dropdown<ToolbarM3ESize>(
label: 'size',
initialOption: ToolbarM3ESize.medium,
options: ToolbarM3ESize.values,
labelBuilder: (v) => v.name,
);
final density = forcedDensity ??
context.knobs.object.dropdown<ToolbarM3EDensity>(
label: 'density',
initialOption: ToolbarM3EDensity.regular,
options: ToolbarM3EDensity.values,
labelBuilder: (v) => v.name,
);
final shape = context.knobs.object.dropdown<ToolbarM3EShapeFamily>(
label: 'shapeFamily',
initialOption: ToolbarM3EShapeFamily.round,
options: ToolbarM3EShapeFamily.values,
labelBuilder: (v) => v.name,
);
final title = forcedTitle ??
context.knobs.string(
label: 'title',
initialValue: longTitle
? 'An exceptionally long toolbar title that will likely overflow the available space'
: 'Toolbar Title',
);
final hasSubtitle =
context.knobs.boolean(label: 'subtitle?', initialValue: false);
final subtitleText = hasSubtitle
? context.knobs.string(label: 'subtitle', initialValue: 'Subheading')
: null;
final actionCount = forcedActionCount ??
context.knobs.int.slider(
label: 'actions',
initialValue: 3,
min: 0,
max: 8,
divisions: 8,
);
final maxInline = context.knobs.int.slider(
label: 'maxInlineActions',
initialValue: 4,
min: 1,
max: 6,
divisions: 5,
);
List<ToolbarActionM3E> buildActions(int count) {
return List.generate(count, (i) {
return ToolbarActionM3E(
icon: i == count - 1 ? Icons.delete : Icons.more_horiz,
isDestructive: i == count - 1,
tooltip: 'Action #${i + 1}',
label: 'Action #${i + 1}',
onPressed: () =>
print('Toolbar action pressed -> index=$i (of $count)'),
);
});
}
return Material(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ToolbarM3E(
leading: withLeading
? IconButton(
onPressed: () => print('Leading pressed'),
icon: const Icon(Icons.menu),
)
: null,
titleText: title,
subtitleText: subtitleText,
actions: buildActions(actionCount),
maxInlineActions: maxInline,
overflowIcon: const Icon(Icons.more_vert),
centerTitle: centerTitle,
variant: variant,
size: size,
density: density,
shapeFamily: shape,
// backgroundColor / foregroundColor left to tokens; can add knob if needed
safeArea: false,
),
// Some filler content to visualize elevation and background separation
Container(
height: 120,
alignment: Alignment.center,
child: const Text('Content area below the toolbar'),
),
],
),
);
}
@UseCase(name: 'default', type: ToolbarM3E)
Widget buildToolbarM3EDefaultUseCase(BuildContext context) {
return _buildToolbarDemo(context);
}
@UseCase(name: 'surface', type: ToolbarM3E)
Widget buildToolbarM3ESurfaceUseCase(BuildContext context) {
return _buildToolbarDemo(context, forcedVariant: ToolbarM3EVariant.surface);
}
@UseCase(name: 'tonal', type: ToolbarM3E)
Widget buildToolbarM3ETonalUseCase(BuildContext context) {
return _buildToolbarDemo(context, forcedVariant: ToolbarM3EVariant.tonal);
}
@UseCase(name: 'primary', type: ToolbarM3E)
Widget buildToolbarM3EPrimaryUseCase(BuildContext context) {
return _buildToolbarDemo(context, forcedVariant: ToolbarM3EVariant.primary);
}
@UseCase(name: 'small', type: ToolbarM3E)
Widget buildToolbarM3ESmallUseCase(BuildContext context) {
return _buildToolbarDemo(context, forcedSize: ToolbarM3ESize.small);
}
@UseCase(name: 'medium', type: ToolbarM3E)
Widget buildToolbarM3EMediumUseCase(BuildContext context) {
return _buildToolbarDemo(context, forcedSize: ToolbarM3ESize.medium);
}
@UseCase(name: 'large', type: ToolbarM3E)
Widget buildToolbarM3ELargeUseCase(BuildContext context) {
return _buildToolbarDemo(context, forcedSize: ToolbarM3ESize.large);
}
@UseCase(name: 'compact', type: ToolbarM3E)
Widget buildToolbarM3ECompactUseCase(BuildContext context) {
return _buildToolbarDemo(context, forcedDensity: ToolbarM3EDensity.compact);
}
@UseCase(name: 'regular', type: ToolbarM3E)
Widget buildToolbarM3ERegularUseCase(BuildContext context) {
return _buildToolbarDemo(context, forcedDensity: ToolbarM3EDensity.regular);
}
@UseCase(name: 'with_leading', type: ToolbarM3E)
Widget buildToolbarM3EWithLeadingUseCase(BuildContext context) {
return _buildToolbarDemo(context, withLeading: true);
}
@UseCase(name: 'with_trailing_actions', type: ToolbarM3E)
Widget buildToolbarM3EWithActionsUseCase(BuildContext context) {
return _buildToolbarDemo(context, forcedActionCount: 3);
}
@UseCase(name: 'with_overflow', type: ToolbarM3E)
Widget buildToolbarM3EWithOverflowUseCase(BuildContext context) {
return _buildToolbarDemo(context, forcedActionCount: 8);
}
@UseCase(name: 'long_title', type: ToolbarM3E)
Widget buildToolbarM3ELongTitleUseCase(BuildContext context) {
return _buildToolbarDemo(context, longTitle: true, centerTitle: false);
}
@UseCase(name: 'centered_title', type: ToolbarM3E)
Widget buildToolbarM3ECenteredTitleUseCase(BuildContext context) {
return _buildToolbarDemo(context, longTitle: false, centerTitle: true);
}

38
widgetbook/pubspec.yaml Normal file
View file

@ -0,0 +1,38 @@
name: widgetbook_workspace
description: Widgetbook workspace for the material_3_expressive monorepo.
publish_to: none
environment:
sdk: ">=3.5.0 <4.0.0"
dependencies:
button_group_m3e:
path: ../packages/button_group_m3e
flutter:
sdk: flutter
loading_indicator_m3e:
path: ../packages/loading_indicator_m3e
m3e_collection:
path: ../packages/m3e_collection
material_new_shapes: ^1.0.0
navigation_bar_m3e:
path: ../packages/navigation_bar_m3e
navigation_rail_m3e:
path: ../packages/navigation_rail_m3e
progress_indicator_m3e:
path: ../packages/progress_indicator_m3e
split_button_m3e:
path: ../packages/split_button_m3e
widgetbook: ^3.18.0
widgetbook_annotation: ^3.7.0
app_bar_m3e:
path: ../packages/app_bar_m3e
fab_m3e:
path: ../packages/fab_m3e
dev_dependencies:
build_runner: ^2.4.13
widgetbook_generator: ^3.18.0
flutter:
uses-material-design: true