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,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);
}