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

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 []);
}