diff --git a/packages/storybook_flutter/example/lib/main.dart b/packages/storybook_flutter/example/lib/main.dart index 24497aeb..9bbbf984 100644 --- a/packages/storybook_flutter/example/lib/main.dart +++ b/packages/storybook_flutter/example/lib/main.dart @@ -4,19 +4,12 @@ import 'package:storybook_flutter_example/router_aware_stories.dart'; void main() => runApp(const MyApp()); -final _plugins = initializePlugins( - contentsSidePanel: true, - knobsSidePanel: true, - initialDeviceFrameData: defaultDeviceFrameData, -); - class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) => Storybook( initialStory: 'Screens/Scaffold', - plugins: _plugins, stories: [ ...routerAwareStories, Story( diff --git a/packages/storybook_flutter/lib/src/plugins/contents/contents.dart b/packages/storybook_flutter/lib/src/plugins/contents/contents.dart index edba9d8c..353a3777 100644 --- a/packages/storybook_flutter/lib/src/plugins/contents/contents.dart +++ b/packages/storybook_flutter/lib/src/plugins/contents/contents.dart @@ -10,51 +10,59 @@ import 'package:storybook_flutter/storybook_flutter.dart'; /// If `sidePanel` is true, the stories are shown in a left side panel, /// otherwise as a popup. class ContentsPlugin extends Plugin { - const ContentsPlugin({bool sidePanel = false}) + const ContentsPlugin() : super( - icon: sidePanel ? null : _buildIcon, - panelBuilder: sidePanel ? null : _buildPanel, - wrapperBuilder: sidePanel ? _buildWrapper : null, + icon: _buildIcon, + panelBuilder: _buildPanel, + wrapperBuilder: _buildWrapper, ); } -Widget _buildIcon(BuildContext _) => const Icon(Icons.list); +Widget? _buildIcon(BuildContext context) => + switch (context.watch()) { + EffectiveLayout.compact => const Icon(Icons.list), + EffectiveLayout.expanded => null, + }; Widget _buildPanel(BuildContext _) => const _Contents(); -Widget _buildWrapper(BuildContext _, Widget? child) => Localizations( - delegates: const [ - DefaultMaterialLocalizations.delegate, - DefaultCupertinoLocalizations.delegate, - DefaultWidgetsLocalizations.delegate, - ], - locale: const Locale('en', 'US'), - child: Directionality( - textDirection: TextDirection.ltr, - child: Row( - children: [ - Material( - child: DecoratedBox( - decoration: const BoxDecoration( - border: Border(right: BorderSide(color: Colors.black12)), - ), - child: SizedBox( - width: 250, - child: Navigator( - onGenerateRoute: (_) => PageRouteBuilder( - pageBuilder: (_, __, ___) => const _Contents(), +Widget _buildWrapper(BuildContext context, Widget? child) => + switch (context.watch()) { + EffectiveLayout.compact => child ?? const SizedBox.shrink(), + EffectiveLayout.expanded => Localizations( + delegates: const [ + DefaultMaterialLocalizations.delegate, + DefaultCupertinoLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + locale: const Locale('en', 'US'), + child: Directionality( + textDirection: TextDirection.ltr, + child: Row( + children: [ + Material( + child: DecoratedBox( + decoration: const BoxDecoration( + border: Border(right: BorderSide(color: Colors.black12)), + ), + child: SizedBox( + width: 250, + child: Navigator( + onGenerateRoute: (_) => PageRouteBuilder( + pageBuilder: (_, __, ___) => const _Contents(), + ), + ), ), ), ), - ), - ), - Expanded( - child: ClipRect(clipBehavior: Clip.hardEdge, child: child), + Expanded( + child: ClipRect(clipBehavior: Clip.hardEdge, child: child), + ), + ], ), - ], + ), ), - ), - ); + }; class _Contents extends StatefulWidget { const _Contents(); diff --git a/packages/storybook_flutter/lib/src/plugins/knobs.dart b/packages/storybook_flutter/lib/src/plugins/knobs.dart index 3ef9e471..a8d79a7c 100644 --- a/packages/storybook_flutter/lib/src/plugins/knobs.dart +++ b/packages/storybook_flutter/lib/src/plugins/knobs.dart @@ -8,19 +8,19 @@ import 'package:storybook_flutter/src/story.dart'; /// /// If `sidePanel` is true, the knobs will be displayed in the right side panel. class KnobsPlugin extends Plugin { - KnobsPlugin({bool sidePanel = false}) + const KnobsPlugin() : super( - icon: sidePanel ? null : _buildIcon, - panelBuilder: sidePanel ? null : _buildPanel, - wrapperBuilder: (context, child) => _buildWrapper( - context, - child, - sidePanel: sidePanel, - ), + icon: _buildIcon, + panelBuilder: _buildPanel, + wrapperBuilder: _buildWrapper, ); } -Widget _buildIcon(BuildContext _) => const Icon(Icons.settings); +Widget? _buildIcon(BuildContext context) => + switch (context.watch()) { + EffectiveLayout.compact => const Icon(Icons.settings), + EffectiveLayout.expanded => null, + }; Widget _buildPanel(BuildContext context) { final knobs = context.watch(); @@ -40,42 +40,38 @@ Widget _buildPanel(BuildContext context) { ); } -Widget _buildWrapper( - BuildContext _, - Widget? child, { - required bool sidePanel, -}) => +Widget _buildWrapper(BuildContext context, Widget? child) => ChangeNotifierProvider( create: (context) => KnobsNotifier(context.read()), - child: sidePanel - ? Directionality( - textDirection: TextDirection.ltr, - child: Row( - children: [ - Expanded(child: child ?? const SizedBox.shrink()), - RepaintBoundary( - child: Material( - child: DecoratedBox( - decoration: const BoxDecoration( - border: Border( - left: BorderSide(color: Colors.black12), - ), + child: switch (context.watch()) { + EffectiveLayout.compact => child, + EffectiveLayout.expanded => Directionality( + textDirection: TextDirection.ltr, + child: Row( + children: [ + Expanded(child: child ?? const SizedBox.shrink()), + RepaintBoundary( + child: Material( + child: DecoratedBox( + decoration: const BoxDecoration( + border: Border( + left: BorderSide(color: Colors.black12), ), - child: SafeArea( - left: false, - child: SizedBox( - width: 250, - child: Localizations( - delegates: const [ - DefaultMaterialLocalizations.delegate, - DefaultWidgetsLocalizations.delegate, - ], - locale: const Locale('en', 'US'), - child: Navigator( - onGenerateRoute: (_) => PageRouteBuilder( - pageBuilder: (context, _, __) => - _buildPanel(context), - ), + ), + child: SafeArea( + left: false, + child: SizedBox( + width: 250, + child: Localizations( + delegates: const [ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + locale: const Locale('en', 'US'), + child: Navigator( + onGenerateRoute: (_) => PageRouteBuilder( + pageBuilder: (context, _, __) => + _buildPanel(context), ), ), ), @@ -83,10 +79,11 @@ Widget _buildWrapper( ), ), ), - ], - ), - ) - : child, + ), + ], + ), + ), + }, ); extension Knobs on BuildContext { diff --git a/packages/storybook_flutter/lib/src/plugins/layout.dart b/packages/storybook_flutter/lib/src/plugins/layout.dart new file mode 100644 index 00000000..046c42b9 --- /dev/null +++ b/packages/storybook_flutter/lib/src/plugins/layout.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:storybook_flutter/storybook_flutter.dart'; + +enum Layout { compact, expanded, auto } + +enum EffectiveLayout { compact, expanded } + +class LayoutProvider extends ValueNotifier { + LayoutProvider(super.value); +} + +class LayoutPlugin extends Plugin { + LayoutPlugin(Layout initialLayout) + : super( + icon: _buildIcon, + wrapperBuilder: (context, child) => + _buildWrapper(context, child, initialLayout), + onPressed: _onPressed, + ); +} + +Widget _buildIcon(BuildContext context) => + switch (context.watch().value) { + Layout.auto => const Icon(Icons.view_carousel), + Layout.compact => const Icon(Icons.view_agenda), + Layout.expanded => const Icon(Icons.width_normal), + }; + +Widget _buildWrapper( + BuildContext _, + Widget? child, + Layout initialLayout, +) => + ChangeNotifierProvider( + create: (context) => LayoutProvider(initialLayout), + child: _EffectiveLayoutBuilder(child: child), + ); + +void _onPressed(BuildContext context) { + final layout = context.read(); + final position = Layout.values.indexOf(layout.value); + layout.value = Layout.values[(position + 1) % Layout.values.length]; +} + +class _EffectiveLayoutBuilder extends StatefulWidget { + const _EffectiveLayoutBuilder({required this.child}); + + final Widget? child; + + @override + State<_EffectiveLayoutBuilder> createState() => + _EffectiveLayoutBuilderState(); +} + +class _EffectiveLayoutBuilderState extends State<_EffectiveLayoutBuilder> { + late EffectiveLayout _layout; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final width = MediaQuery.sizeOf(context).width; + _layout = switch (context.watch().value) { + Layout.auto => + width < 800 ? EffectiveLayout.compact : EffectiveLayout.expanded, + Layout.compact => EffectiveLayout.compact, + Layout.expanded => EffectiveLayout.expanded, + }; + } + + @override + Widget build(BuildContext context) => Provider.value( + value: _layout, + child: widget.child, + ); +} diff --git a/packages/storybook_flutter/lib/src/plugins/plugin.dart b/packages/storybook_flutter/lib/src/plugins/plugin.dart index 6fabc07b..fd6899f7 100644 --- a/packages/storybook_flutter/lib/src/plugins/plugin.dart +++ b/packages/storybook_flutter/lib/src/plugins/plugin.dart @@ -1,33 +1,27 @@ import 'package:flutter/widgets.dart'; -import 'package:storybook_flutter/src/plugins/contents/contents.dart'; import 'package:storybook_flutter/src/plugins/device_frame.dart'; -import 'package:storybook_flutter/src/plugins/knobs.dart'; import 'package:storybook_flutter/src/plugins/theme_mode.dart'; export 'contents/contents.dart'; export 'device_frame.dart'; export 'knobs.dart'; +export 'layout.dart'; export 'theme_mode.dart'; /// Use this method to initialize and customize built-in plugins. List initializePlugins({ - bool enableContents = true, - bool enableKnobs = true, bool enableThemeMode = true, bool enableDeviceFrame = true, DeviceFrameData initialDeviceFrameData = defaultDeviceFrameData, - bool contentsSidePanel = false, - bool knobsSidePanel = false, }) => [ - if (enableContents) ContentsPlugin(sidePanel: contentsSidePanel), - if (enableKnobs) KnobsPlugin(sidePanel: knobsSidePanel), if (enableThemeMode) ThemeModePlugin(), if (enableDeviceFrame) DeviceFramePlugin(initialData: initialDeviceFrameData), ]; typedef OnPluginButtonPressed = void Function(BuildContext context); +typedef NullableWidgetBuilder = Widget? Function(BuildContext context); class Plugin { const Plugin({ @@ -56,7 +50,7 @@ class Plugin { final TransitionBuilder? storyBuilder; /// Optional icon that will be displayed on the bottom panel. - final WidgetBuilder? icon; + final NullableWidgetBuilder? icon; /// Optional callback that will be called when user clicks on the [icon]. /// diff --git a/packages/storybook_flutter/lib/src/plugins/plugin_panel.dart b/packages/storybook_flutter/lib/src/plugins/plugin_panel.dart index ae252406..302a352f 100644 --- a/packages/storybook_flutter/lib/src/plugins/plugin_panel.dart +++ b/packages/storybook_flutter/lib/src/plugins/plugin_panel.dart @@ -22,6 +22,12 @@ class PluginPanel extends StatefulWidget { class _PluginPanelState extends State { PluginOverlay? _overlay; + @override + void dispose() { + _overlay?.remove(); + super.dispose(); + } + OverlayEntry _createEntry(WidgetBuilder childBuilder) => OverlayEntry( builder: (context) => Provider( create: (context) => OverlayController( @@ -98,12 +104,12 @@ class _PluginPanelState extends State { Widget build(BuildContext context) => Wrap( runAlignment: WrapAlignment.center, children: widget.plugins - .where((p) => p.icon != null) + .map((p) => (p, p.icon?.call(context))) + .whereType<(Plugin, Widget)>() .map( (p) => IconButton( - // ignore: avoid-non-null-assertion, checked for null - icon: p.icon!.call(context), - onPressed: () => _onPluginButtonPressed(p), + icon: p.$2, + onPressed: () => _onPluginButtonPressed(p.$1), ), ) .toList(), diff --git a/packages/storybook_flutter/lib/src/storybook.dart b/packages/storybook_flutter/lib/src/storybook.dart index c029ef57..2323d810 100644 --- a/packages/storybook_flutter/lib/src/storybook.dart +++ b/packages/storybook_flutter/lib/src/storybook.dart @@ -31,7 +31,13 @@ class Storybook extends StatefulWidget { this.initialStory, this.wrapperBuilder = materialWrapper, this.showPanel = true, - }) : plugins = UnmodifiableListView(plugins ?? _defaultPlugins), + Layout initialLayout = Layout.auto, + }) : plugins = UnmodifiableListView([ + LayoutPlugin(initialLayout), + const ContentsPlugin(), + const KnobsPlugin(), + ...plugins ?? _defaultPlugins, + ]), stories = UnmodifiableListView(stories); /// All enabled plugins. diff --git a/packages/storybook_flutter/test/golden_test.dart b/packages/storybook_flutter/test/golden_test.dart index dfd7517f..8d943e49 100644 --- a/packages/storybook_flutter/test/golden_test.dart +++ b/packages/storybook_flutter/test/golden_test.dart @@ -3,10 +3,7 @@ import 'package:golden_toolkit/golden_toolkit.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; Widget simpleStorybook(String initialStory) => Storybook( - plugins: initializePlugins( - contentsSidePanel: true, - knobsSidePanel: true, - ), + plugins: initializePlugins(), initialStory: initialStory, stories: [ Story( diff --git a/packages/storybook_flutter/test/goldens/simple_story_layout.png b/packages/storybook_flutter/test/goldens/simple_story_layout.png index 6e74e666..66f3625c 100644 Binary files a/packages/storybook_flutter/test/goldens/simple_story_layout.png and b/packages/storybook_flutter/test/goldens/simple_story_layout.png differ diff --git a/packages/storybook_flutter/test/goldens/story_layout.png b/packages/storybook_flutter/test/goldens/story_layout.png index 0031e0cd..3e53810a 100644 Binary files a/packages/storybook_flutter/test/goldens/story_layout.png and b/packages/storybook_flutter/test/goldens/story_layout.png differ