From 55e9376ed5e085e03438c5a92d002842cd804482 Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Thu, 31 Jul 2025 16:47:30 +0200 Subject: [PATCH 01/22] Initial tree implementation --- core/ui_showcase/.gitignore | 31 ++++ core/ui_showcase/.metadata | 10 ++ .../lib/src/core/showcase_node.dart | 165 ++++++++++++++++++ core/ui_showcase/lib/ui_showcase.dart | 3 + core/ui_showcase/pubspec.yaml | 54 ++++++ 5 files changed, 263 insertions(+) create mode 100644 core/ui_showcase/.gitignore create mode 100644 core/ui_showcase/.metadata create mode 100644 core/ui_showcase/lib/src/core/showcase_node.dart create mode 100644 core/ui_showcase/lib/ui_showcase.dart create mode 100644 core/ui_showcase/pubspec.yaml diff --git a/core/ui_showcase/.gitignore b/core/ui_showcase/.gitignore new file mode 100644 index 00000000..eb6c05cd --- /dev/null +++ b/core/ui_showcase/.gitignore @@ -0,0 +1,31 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +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 +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +build/ diff --git a/core/ui_showcase/.metadata b/core/ui_showcase/.metadata new file mode 100644 index 00000000..91a5e0b2 --- /dev/null +++ b/core/ui_showcase/.metadata @@ -0,0 +1,10 @@ +# 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: "be698c48a6750c8cb8e61c740ca9991bb947aba2" + channel: "stable" + +project_type: package diff --git a/core/ui_showcase/lib/src/core/showcase_node.dart b/core/ui_showcase/lib/src/core/showcase_node.dart new file mode 100644 index 00000000..99bed044 --- /dev/null +++ b/core/ui_showcase/lib/src/core/showcase_node.dart @@ -0,0 +1,165 @@ +import 'package:flutter/material.dart'; + +abstract class ShowcaseNode { + // Private fields + ShowcaseNode? _parent; + int _depth = 0; + + // Abstract getters (must be implemented by subclasses) + String get name; + + // Virtual getters (can be overridden by subclasses) + String? get description => null; + List get tags => const []; + String? get category => null; + bool get isDeprecated => false; + Widget? get widget => null; + + // Computed getters + /// Get all children (empty for leaf nodes) + List get children => const []; + + /// Get parent node + ShowcaseNode? get parent => _parent; + + /// Get depth in the tree + int get depth => _depth; + + /// Check if this node is a leaf (has no children) + bool get isLeaf => children.isEmpty; + + /// Full path from root to this node + String get path { + if (_parent == null) return _encodeNameToPath(name); + return '${_parent!.path}/${_encodeNameToPath(name)}'; + } + + String _encodeNameToPath(String name) { + return name.toLowerCase().replaceAll(' ', '-'); + } + + // Tree manipulation methods + void adoptChild(ShowcaseNode child) { + assert(child._parent == null, 'Node already has a parent'); + child._parent = this; + redepthChild(child); + } + + void dropChild(ShowcaseNode child) { + child._parent = null; + } + + void redepthChild(ShowcaseNode child) { + if (child._depth <= _depth) { + child._depth = _depth + 1; + child.redepthChildren(); + } + } + + @protected + void redepthChildren() {} + + void visitChildren(void Function(ShowcaseNode child) visitor) { + children.forEach(visitor); + } + + // Navigation and search methods + /// Find a node by path + ShowcaseNode? findByPath(String path) { + if (this.path == path) return this; + + ShowcaseNode? result; + visitChildren((child) { + result ??= child.findByPath(path); + }); + return result; + } + + /// Get breadcrumb path to this node + List getBreadcrumbs() { + final breadcrumbs = []; + ShowcaseNode? current = this; + + while (current != null) { + breadcrumbs.insert(0, current); + current = current.parent; + } + + return breadcrumbs; + } +} + +class ShowcaseFolderNode extends ShowcaseNode { + ShowcaseFolderNode({ + required this.name, + required this.children, + this.description, + this.tags = const [], + this.category, + this.isDeprecated = false, + }) { + children.forEach(adoptChild); + } + + // Override properties + @override + final String name; + + @override + final String? description; + + @override + final String? category; + + @override + final List tags; + + @override + final bool isDeprecated; + + @override + final List children; + + // Override methods + @override + void redepthChildren() { + for (final child in children) { + redepthChild(child); + } + } + + @override + void visitChildren(void Function(ShowcaseNode child) visitor) { + children.forEach(visitor); + } +} + +class ShowcasePreviewNode extends ShowcaseNode { + ShowcasePreviewNode({ + required this.name, + required this.widget, + this.description, + this.tags = const [], + this.category, + this.isDeprecated = false, + }); + + // Override properties + @override + final String name; + + @override + final String? description; + + @override + final String? category; + + @override + final List tags; + + @override + final bool isDeprecated; + + @override + final Widget widget; +} diff --git a/core/ui_showcase/lib/ui_showcase.dart b/core/ui_showcase/lib/ui_showcase.dart new file mode 100644 index 00000000..abbb0ea3 --- /dev/null +++ b/core/ui_showcase/lib/ui_showcase.dart @@ -0,0 +1,3 @@ +library ui_showcase; + +export 'src/core/showcase_node.dart'; diff --git a/core/ui_showcase/pubspec.yaml b/core/ui_showcase/pubspec.yaml new file mode 100644 index 00000000..438312d3 --- /dev/null +++ b/core/ui_showcase/pubspec.yaml @@ -0,0 +1,54 @@ +name: ui_showcase +description: "A new Flutter package project." +version: 0.0.1 +homepage: + +environment: + sdk: ^3.8.0 + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package From 74dbc4c4eebeebb47c1c8ff7ae6b0c60ae7764c9 Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Thu, 31 Jul 2025 17:39:46 +0200 Subject: [PATCH 02/22] Implemented tree view --- core/ui_library/example/.gitignore | 45 ++++ core/ui_library/example/.metadata | 30 +++ core/ui_library/example/README.md | 3 + core/ui_library/example/lib/main.dart | 17 ++ .../lib/src/previews/button_preview.dart | 10 + .../example/lib/src/widget/showcase_root.dart | 35 +++ core/ui_library/example/pubspec.lock | 204 ++++++++++++++++++ core/ui_library/example/pubspec.yaml | 19 ++ .../lib/src/widget/showcase_tree_view.dart | 91 ++++++++ core/ui_showcase/lib/ui_showcase.dart | 3 +- core/ui_showcase/pubspec.yaml | 1 + 11 files changed, 456 insertions(+), 2 deletions(-) create mode 100644 core/ui_library/example/.gitignore create mode 100644 core/ui_library/example/.metadata create mode 100644 core/ui_library/example/README.md create mode 100644 core/ui_library/example/lib/main.dart create mode 100644 core/ui_library/example/lib/src/previews/button_preview.dart create mode 100644 core/ui_library/example/lib/src/widget/showcase_root.dart create mode 100644 core/ui_library/example/pubspec.lock create mode 100644 core/ui_library/example/pubspec.yaml create mode 100644 core/ui_showcase/lib/src/widget/showcase_tree_view.dart diff --git a/core/ui_library/example/.gitignore b/core/ui_library/example/.gitignore new file mode 100644 index 00000000..79c113f9 --- /dev/null +++ b/core/ui_library/example/.gitignore @@ -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 +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# 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 diff --git a/core/ui_library/example/.metadata b/core/ui_library/example/.metadata new file mode 100644 index 00000000..1bac95ed --- /dev/null +++ b/core/ui_library/example/.metadata @@ -0,0 +1,30 @@ +# 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: "be698c48a6750c8cb8e61c740ca9991bb947aba2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + - platform: macos + create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + + # 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' diff --git a/core/ui_library/example/README.md b/core/ui_library/example/README.md new file mode 100644 index 00000000..1b7a4e3d --- /dev/null +++ b/core/ui_library/example/README.md @@ -0,0 +1,3 @@ +# example + +A new Flutter project. diff --git a/core/ui_library/example/lib/main.dart b/core/ui_library/example/lib/main.dart new file mode 100644 index 00000000..76b61be5 --- /dev/null +++ b/core/ui_library/example/lib/main.dart @@ -0,0 +1,17 @@ +import 'package:example/src/widget/showcase_root.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: ShowcaseRoot(), + ); + } +} diff --git a/core/ui_library/example/lib/src/previews/button_preview.dart b/core/ui_library/example/lib/src/previews/button_preview.dart new file mode 100644 index 00000000..f45b8499 --- /dev/null +++ b/core/ui_library/example/lib/src/previews/button_preview.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class SomeButtonPreview extends StatelessWidget { + const SomeButtonPreview({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/core/ui_library/example/lib/src/widget/showcase_root.dart b/core/ui_library/example/lib/src/widget/showcase_root.dart new file mode 100644 index 00000000..033489be --- /dev/null +++ b/core/ui_library/example/lib/src/widget/showcase_root.dart @@ -0,0 +1,35 @@ +import 'package:example/src/previews/button_preview.dart'; +import 'package:flutter/material.dart'; +import 'package:ui_showcase/ui_showcase.dart'; + +final showcaseNodes = [ + ShowcaseFolderNode( + name: 'Button', + children: [ + ShowcasePreviewNode( + name: 'Variant 1', + widget: const SomeButtonPreview(), + ), + ShowcasePreviewNode( + name: 'Variant 2', + widget: const SomeButtonPreview(), + ), + ], + ), +]; + +class ShowcaseRoot extends StatelessWidget { + const ShowcaseRoot({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Row( + children: [ + SizedBox(width: 200, child: ShowcaseTreeView(nodes: showcaseNodes)), + Expanded(child: Container(color: Colors.red)), + ], + ), + ); + } +} diff --git a/core/ui_library/example/pubspec.lock b/core/ui_library/example/pubspec.lock new file mode 100644 index 00000000..50fa2a7e --- /dev/null +++ b/core/ui_library/example/pubspec.lock @@ -0,0 +1,204 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + url: "https://pub.dev" + source: hosted + version: "10.0.9" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + two_dimensional_scrollables: + dependency: transitive + description: + name: two_dimensional_scrollables + sha256: "0f77ecb96596f2f82eec2b0a8e60d9305c58315557da9fa3b610c7dbf5ded621" + url: "https://pub.dev" + source: hosted + version: "0.3.7" + ui_showcase: + dependency: "direct main" + description: + path: "../../ui_showcase" + relative: true + source: path + version: "0.0.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" +sdks: + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/core/ui_library/example/pubspec.yaml b/core/ui_library/example/pubspec.yaml new file mode 100644 index 00000000..cc660969 --- /dev/null +++ b/core/ui_library/example/pubspec.yaml @@ -0,0 +1,19 @@ +name: example +description: "A new Flutter project." +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: ^3.8.0 + +dependencies: + flutter: + sdk: flutter + ui_showcase: + path: ../../ui_showcase + +dev_dependencies: + flutter_test: + sdk: flutter +flutter: + uses-material-design: true diff --git a/core/ui_showcase/lib/src/widget/showcase_tree_view.dart b/core/ui_showcase/lib/src/widget/showcase_tree_view.dart new file mode 100644 index 00000000..3f966655 --- /dev/null +++ b/core/ui_showcase/lib/src/widget/showcase_tree_view.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart'; +import 'package:ui_showcase/ui_showcase.dart'; + +class ShowcaseTreeView extends StatefulWidget { + const ShowcaseTreeView({ + required this.nodes, + this.selectedNode, + this.onNodeSelected, + super.key, + }); + + final List nodes; + final ShowcaseNode? selectedNode; + final ValueChanged? onNodeSelected; + + static Widget _nodeBuilder({ + required BuildContext context, + required TreeViewNode node, + required AnimationStyle animationStyle, + required ValueChanged> onNodeSelected, + }) { + final animationDuration = animationStyle.duration ?? TreeView.defaultAnimationDuration; + final animationCurve = animationStyle.curve ?? TreeView.defaultAnimationCurve; + + return Padding( + padding: const EdgeInsets.only(left: 8), + child: GestureDetector( + onTap: () => onNodeSelected(node), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 4, + children: [ + if (node.children.isNotEmpty) + AnimatedSwitcher( + duration: animationDuration, + switchInCurve: animationCurve, + switchOutCurve: animationCurve, + child: Icon( + node.isExpanded ? Icons.folder_open : Icons.folder, + key: ValueKey(node.isExpanded), + ), + ), + Text(node.content.name), + ], + ), + ), + ); + } + + @override + State createState() => _ShowcaseTreeViewState(); +} + +class _ShowcaseTreeViewState extends State { + late final _nodes = _buildTree(widget.nodes); + + List> _buildTree(List nodes) { + return nodes + .map((node) => TreeViewNode(node, children: _buildTree(node.children))) + .toList(growable: false); + } + + void _onNodeSelected(BuildContext context, TreeViewNode node) { + if (node.children.isNotEmpty) { + TreeViewController.of(context).toggleNode(node); + return; + } + + widget.onNodeSelected?.call(node.content); + } + + @override + Widget build(BuildContext context) { + return TreeView( + tree: _nodes, + treeRowBuilder: (node) => const TreeRow(extent: FixedTreeRowExtent(32)), + treeNodeBuilder: (context, node, style) => ShowcaseTreeView._nodeBuilder( + context: context, + node: node, + animationStyle: style, + onNodeSelected: (node) => _onNodeSelected(context, node), + ), + toggleAnimationStyle: const AnimationStyle( + duration: Duration(milliseconds: 100), + curve: Curves.easeInOut, + reverseCurve: Curves.easeInOut, + ), + ); + } +} diff --git a/core/ui_showcase/lib/ui_showcase.dart b/core/ui_showcase/lib/ui_showcase.dart index abbb0ea3..6f8f27e8 100644 --- a/core/ui_showcase/lib/ui_showcase.dart +++ b/core/ui_showcase/lib/ui_showcase.dart @@ -1,3 +1,2 @@ -library ui_showcase; - export 'src/core/showcase_node.dart'; +export 'src/widget/showcase_tree_view.dart'; diff --git a/core/ui_showcase/pubspec.yaml b/core/ui_showcase/pubspec.yaml index 438312d3..bdd5d7d8 100644 --- a/core/ui_showcase/pubspec.yaml +++ b/core/ui_showcase/pubspec.yaml @@ -10,6 +10,7 @@ environment: dependencies: flutter: sdk: flutter + two_dimensional_scrollables: ^0.3.7 dev_dependencies: flutter_test: From 84b134b6c6ee996fc008d1eee543356f3587c626 Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Thu, 31 Jul 2025 20:32:05 +0200 Subject: [PATCH 03/22] Enhance button preview and showcase root functionality - Updated `SomeButtonPreview` to display an `ElevatedButton` with an app bar showing the active node's path. - Refactored `ShowcaseRoot` to manage selected nodes using state, allowing dynamic updates to the displayed widget based on user selection. - Introduced `ActiveNodeProvider` to manage the active node context across the widget tree. --- .../lib/src/previews/button_preview.dart | 15 ++++++- .../example/lib/src/widget/showcase_root.dart | 41 ++++++++++++++----- .../lib/src/widget/active_node_provider.dart | 28 +++++++++++++ core/ui_showcase/lib/ui_showcase.dart | 1 + 4 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 core/ui_showcase/lib/src/widget/active_node_provider.dart diff --git a/core/ui_library/example/lib/src/previews/button_preview.dart b/core/ui_library/example/lib/src/previews/button_preview.dart index f45b8499..f444a2fd 100644 --- a/core/ui_library/example/lib/src/previews/button_preview.dart +++ b/core/ui_library/example/lib/src/previews/button_preview.dart @@ -1,10 +1,23 @@ import 'package:flutter/material.dart'; +import 'package:ui_showcase/ui_showcase.dart'; class SomeButtonPreview extends StatelessWidget { const SomeButtonPreview({super.key}); @override Widget build(BuildContext context) { - return const Placeholder(); + final activeNode = ActiveNodeProvider.of(context).activeNode!; + + return Scaffold( + appBar: AppBar( + title: Text(activeNode.path), + ), + body: Center( + child: ElevatedButton( + onPressed: () {}, + child: const Text('Click me'), + ), + ), + ); } } diff --git a/core/ui_library/example/lib/src/widget/showcase_root.dart b/core/ui_library/example/lib/src/widget/showcase_root.dart index 033489be..f0579f69 100644 --- a/core/ui_library/example/lib/src/widget/showcase_root.dart +++ b/core/ui_library/example/lib/src/widget/showcase_root.dart @@ -6,28 +6,47 @@ final showcaseNodes = [ ShowcaseFolderNode( name: 'Button', children: [ - ShowcasePreviewNode( - name: 'Variant 1', - widget: const SomeButtonPreview(), - ), - ShowcasePreviewNode( - name: 'Variant 2', - widget: const SomeButtonPreview(), - ), + ShowcasePreviewNode(name: 'Variant 1', widget: const SomeButtonPreview()), ], ), ]; -class ShowcaseRoot extends StatelessWidget { +class ShowcaseRoot extends StatefulWidget { const ShowcaseRoot({super.key}); + @override + State createState() => _ShowcaseRootState(); +} + +class _ShowcaseRootState extends State { + ShowcaseNode? _selectedNode; + + void _onNodeSelected(ShowcaseNode node) { + setState(() { + _selectedNode = node; + }); + } + @override Widget build(BuildContext context) { return Scaffold( body: Row( children: [ - SizedBox(width: 200, child: ShowcaseTreeView(nodes: showcaseNodes)), - Expanded(child: Container(color: Colors.red)), + SizedBox( + width: 200, + child: ShowcaseTreeView( + nodes: showcaseNodes, + selectedNode: _selectedNode, + onNodeSelected: _onNodeSelected, + ), + ), + if (_selectedNode case final node?) + ActiveNodeProvider( + activeNode: node, + child: Expanded( + child: node.widget ?? const SizedBox.shrink(), + ), + ), ], ), ); diff --git a/core/ui_showcase/lib/src/widget/active_node_provider.dart b/core/ui_showcase/lib/src/widget/active_node_provider.dart new file mode 100644 index 00000000..ec7316fa --- /dev/null +++ b/core/ui_showcase/lib/src/widget/active_node_provider.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:ui_showcase/ui_showcase.dart'; + +class ActiveNodeProvider extends InheritedWidget { + const ActiveNodeProvider({ + required this.activeNode, + required super.child, + super.key, + }); + + final ShowcaseNode? activeNode; + + static ActiveNodeProvider? maybeOf(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } + + static ActiveNodeProvider of(BuildContext context) { + final result = maybeOf(context); + assert(result != null, 'No ActiveNodeProvider found in context'); + + return result!; + } + + @override + bool updateShouldNotify(ActiveNodeProvider oldWidget) { + return activeNode != oldWidget.activeNode; + } +} diff --git a/core/ui_showcase/lib/ui_showcase.dart b/core/ui_showcase/lib/ui_showcase.dart index 6f8f27e8..76825df3 100644 --- a/core/ui_showcase/lib/ui_showcase.dart +++ b/core/ui_showcase/lib/ui_showcase.dart @@ -1,2 +1,3 @@ export 'src/core/showcase_node.dart'; +export 'src/widget/active_node_provider.dart'; export 'src/widget/showcase_tree_view.dart'; From 44bbc6f36acab00598cdfd632b353445c154383f Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Sat, 2 Aug 2025 09:57:46 +0200 Subject: [PATCH 04/22] wip --- .vscode/launch.json | 7 + core/ui_library/example/lib/main.dart | 45 ++++++- .../lib/src/previews/button_preview.dart | 4 +- .../example/lib/src/widget/showcase_root.dart | 54 -------- core/ui_library/example/pubspec.lock | 21 +++ .../lib/src/core/showcase_node.dart | 29 ++++- .../lib/src/core/showcase_nodes.dart | 38 ++++++ .../lib/src/routing/go_router.dart | 44 +++++++ .../lib/src/widget/active_node_notifier.dart | 92 ++++++++++++++ .../lib/src/widget/active_node_provider.dart | 28 ---- .../lib/src/widget/showcase_tree_view.dart | 63 +++++++-- .../lib/src/widget/showcase_view.dart | 120 ++++++++++++++++++ core/ui_showcase/lib/ui_showcase.dart | 5 +- core/ui_showcase/pubspec.yaml | 41 +----- 14 files changed, 443 insertions(+), 148 deletions(-) delete mode 100644 core/ui_library/example/lib/src/widget/showcase_root.dart create mode 100644 core/ui_showcase/lib/src/core/showcase_nodes.dart create mode 100644 core/ui_showcase/lib/src/routing/go_router.dart create mode 100644 core/ui_showcase/lib/src/widget/active_node_notifier.dart delete mode 100644 core/ui_showcase/lib/src/widget/active_node_provider.dart create mode 100644 core/ui_showcase/lib/src/widget/showcase_view.dart diff --git a/.vscode/launch.json b/.vscode/launch.json index 82d56d42..532ffaaf 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,6 +20,13 @@ "type": "dart", "cwd": "${workspaceFolder}/app", "flutterMode": "release" + }, + { + "name": "ui_library_showcase", + "request": "launch", + "type": "dart", + "cwd": "${workspaceFolder}/core/ui_library/example", + "program": "lib/main.dart" } ] } \ No newline at end of file diff --git a/core/ui_library/example/lib/main.dart b/core/ui_library/example/lib/main.dart index 76b61be5..17e5a41a 100644 --- a/core/ui_library/example/lib/main.dart +++ b/core/ui_library/example/lib/main.dart @@ -1,17 +1,54 @@ -import 'package:example/src/widget/showcase_root.dart'; +import 'package:example/src/previews/button_preview.dart'; import 'package:flutter/material.dart'; +import 'package:ui_showcase/ui_showcase.dart'; void main() { runApp(const MyApp()); } -class MyApp extends StatelessWidget { +final showcaseNodes = [ + ShowcaseFolderNode( + name: 'Button', + children: [ + ShowcasePreviewNode(name: 'Variant 1', widget: const SomeButtonPreview()), + ShowcasePreviewNode(name: 'Variant 2', widget: const SomeButtonPreview()), + ShowcasePreviewNode(name: 'Variant 3', widget: const SomeButtonPreview()), + ShowcaseFolderNode( + name: 'Folder', + children: [ + ShowcasePreviewNode(name: 'Variant 1', widget: const SomeButtonPreview()), + ShowcasePreviewNode(name: 'Variant 2', widget: const SomeButtonPreview()), + ], + ), + ], + ), +]; + +class MyApp extends StatefulWidget { const MyApp({super.key}); + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + late final RouterConfig router; + + @override + void initState() { + super.initState(); + router = createRouter(showcaseNodes); + } + @override Widget build(BuildContext context) { - return const MaterialApp( - home: ShowcaseRoot(), + final darkTheme = ThemeData.dark(); + final lightTheme = ThemeData.light(); + + return MaterialApp.router( + routerConfig: router, + theme: lightTheme, + darkTheme: darkTheme, ); } } diff --git a/core/ui_library/example/lib/src/previews/button_preview.dart b/core/ui_library/example/lib/src/previews/button_preview.dart index f444a2fd..4e4ad9e1 100644 --- a/core/ui_library/example/lib/src/previews/button_preview.dart +++ b/core/ui_library/example/lib/src/previews/button_preview.dart @@ -9,9 +9,7 @@ class SomeButtonPreview extends StatelessWidget { final activeNode = ActiveNodeProvider.of(context).activeNode!; return Scaffold( - appBar: AppBar( - title: Text(activeNode.path), - ), + appBar: AppBar(title: Text(activeNode.fullPath)), body: Center( child: ElevatedButton( onPressed: () {}, diff --git a/core/ui_library/example/lib/src/widget/showcase_root.dart b/core/ui_library/example/lib/src/widget/showcase_root.dart deleted file mode 100644 index f0579f69..00000000 --- a/core/ui_library/example/lib/src/widget/showcase_root.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:example/src/previews/button_preview.dart'; -import 'package:flutter/material.dart'; -import 'package:ui_showcase/ui_showcase.dart'; - -final showcaseNodes = [ - ShowcaseFolderNode( - name: 'Button', - children: [ - ShowcasePreviewNode(name: 'Variant 1', widget: const SomeButtonPreview()), - ], - ), -]; - -class ShowcaseRoot extends StatefulWidget { - const ShowcaseRoot({super.key}); - - @override - State createState() => _ShowcaseRootState(); -} - -class _ShowcaseRootState extends State { - ShowcaseNode? _selectedNode; - - void _onNodeSelected(ShowcaseNode node) { - setState(() { - _selectedNode = node; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Row( - children: [ - SizedBox( - width: 200, - child: ShowcaseTreeView( - nodes: showcaseNodes, - selectedNode: _selectedNode, - onNodeSelected: _onNodeSelected, - ), - ), - if (_selectedNode case final node?) - ActiveNodeProvider( - activeNode: node, - child: Expanded( - child: node.widget ?? const SizedBox.shrink(), - ), - ), - ], - ), - ); - } -} diff --git a/core/ui_library/example/pubspec.lock b/core/ui_library/example/pubspec.lock index 50fa2a7e..8621db2a 100644 --- a/core/ui_library/example/pubspec.lock +++ b/core/ui_library/example/pubspec.lock @@ -59,6 +59,19 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + go_router: + dependency: transitive + description: + name: go_router + sha256: c489908a54ce2131f1d1b7cc631af9c1a06fac5ca7c449e959192089f9489431 + url: "https://pub.dev" + source: hosted + version: "16.0.0" leak_tracker: dependency: transitive description: @@ -83,6 +96,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: diff --git a/core/ui_showcase/lib/src/core/showcase_node.dart b/core/ui_showcase/lib/src/core/showcase_node.dart index 99bed044..58cfdce8 100644 --- a/core/ui_showcase/lib/src/core/showcase_node.dart +++ b/core/ui_showcase/lib/src/core/showcase_node.dart @@ -29,9 +29,13 @@ abstract class ShowcaseNode { bool get isLeaf => children.isEmpty; /// Full path from root to this node - String get path { + String get fullPath { if (_parent == null) return _encodeNameToPath(name); - return '${_parent!.path}/${_encodeNameToPath(name)}'; + return '${_parent!.fullPath}/${_encodeNameToPath(name)}'; + } + + String get path { + return _encodeNameToPath(name); } String _encodeNameToPath(String name) { @@ -66,7 +70,7 @@ abstract class ShowcaseNode { // Navigation and search methods /// Find a node by path ShowcaseNode? findByPath(String path) { - if (this.path == path) return this; + if (fullPath == path) return this; ShowcaseNode? result; visitChildren((child) { @@ -75,6 +79,16 @@ abstract class ShowcaseNode { return result; } + ShowcaseNode? findByName(String name) { + if (this.name == name) return this; + + ShowcaseNode? result; + visitChildren((child) { + result ??= child.findByName(name); + }); + return result; + } + /// Get breadcrumb path to this node List getBreadcrumbs() { final breadcrumbs = []; @@ -97,11 +111,8 @@ class ShowcaseFolderNode extends ShowcaseNode { this.tags = const [], this.category, this.isDeprecated = false, - }) { - children.forEach(adoptChild); - } + }); - // Override properties @override final String name; @@ -131,6 +142,10 @@ class ShowcaseFolderNode extends ShowcaseNode { @override void visitChildren(void Function(ShowcaseNode child) visitor) { children.forEach(visitor); + + for (final child in children) { + child.visitChildren(visitor); + } } } diff --git a/core/ui_showcase/lib/src/core/showcase_nodes.dart b/core/ui_showcase/lib/src/core/showcase_nodes.dart new file mode 100644 index 00000000..8206f070 --- /dev/null +++ b/core/ui_showcase/lib/src/core/showcase_nodes.dart @@ -0,0 +1,38 @@ +import 'package:ui_showcase/ui_showcase.dart'; + +extension type ShowcaseNodes(List nodes) { + ShowcaseNode? findNodeByName(String name) { + for (final node in nodes) { + if (node.name == name) return node; + ShowcaseNode? found; + + node.visitChildren((child) { + if (child.name == name) { + found = child; + } + }); + + if (found != null) return found; + } + + return null; + } + + ShowcaseNode? findNodeByPath(String path) { + for (final node in nodes) { + if (node.fullPath == path) return node; + ShowcaseNode? found; + + node.visitChildren((child) { + print('child.fullPath: ${child.fullPath} path: $path'); + if (child.fullPath == path) { + found = child; + } + }); + + if (found != null) return found; + } + + return null; + } +} diff --git a/core/ui_showcase/lib/src/routing/go_router.dart b/core/ui_showcase/lib/src/routing/go_router.dart new file mode 100644 index 00000000..fe65bfbd --- /dev/null +++ b/core/ui_showcase/lib/src/routing/go_router.dart @@ -0,0 +1,44 @@ +import 'package:flutter/widgets.dart'; +import 'package:go_router/go_router.dart'; +import 'package:ui_showcase/ui_showcase.dart'; + +GoRouter createRouter(List nodes) { + return GoRouter( + debugLogDiagnostics: true, + routes: [ + ShellRoute( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const SizedBox.shrink(), + routes: createRoutes(nodes), + ), + ], + builder: (context, state, child) => ShowcaseView( + nodes: nodes, + navigator: child, + ), + ), + ], + ); +} + +List createRoutes(List nodes) { + final routes = []; + + for (final node in nodes) { + final childrenRoutes = node.children.isEmpty ? [] : createRoutes(node.children); + + routes.add( + GoRoute( + path: node.path, + pageBuilder: (context, state) { + return NoTransitionPage(child: node.widget ?? const SizedBox.shrink()); + }, + routes: childrenRoutes, + ), + ); + } + + return routes; +} diff --git a/core/ui_showcase/lib/src/widget/active_node_notifier.dart b/core/ui_showcase/lib/src/widget/active_node_notifier.dart new file mode 100644 index 00000000..51c6e59a --- /dev/null +++ b/core/ui_showcase/lib/src/widget/active_node_notifier.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:ui_showcase/ui_showcase.dart'; + +class ActiveNodeProvider extends StatefulWidget { + const ActiveNodeProvider({ + required this.nodes, + required this.child, + super.key, + }); + + final List nodes; + final Widget child; + + static ActiveNodeProviderState of(BuildContext context, {bool listen = true}) { + final widget = listen + ? context.dependOnInheritedWidgetOfExactType<_InheritedNodeNotifier>() + : context.getInheritedWidgetOfExactType<_InheritedNodeNotifier>(); + + if (widget == null) { + throw FlutterError('No ActiveNodeProvider found in context'); + } + + return widget.state; + } + + @override + State createState() => ActiveNodeProviderState(); +} + +class ActiveNodeProviderState extends State { + late final ActiveNodeNotifier _notifier; + late final GoRouter _router; + + @override + void initState() { + super.initState(); + _router = GoRouter.of(context); + _notifier = ActiveNodeNotifier(router: _router, nodes: widget.nodes); + } + + ShowcaseNode? get activeNode => _notifier.value; + + void updateActiveNode(ShowcaseNode? node) { + _router.go('/${node?.fullPath ?? ''}'); + } + + @override + Widget build(BuildContext context) { + return _InheritedNodeNotifier( + state: this, + notifier: _notifier, + child: widget.child, + ); + } +} + +class ActiveNodeNotifier extends ValueNotifier { + ActiveNodeNotifier({ + required this.router, + required this.nodes, + }) : super(_nodeFromPath(router.state.fullPath ?? '', nodes)) { + router.routerDelegate.addListener(_onRouteChange); + } + + final GoRouter router; + final List nodes; + + static ShowcaseNode? _nodeFromPath(String path, List nodes) { + return ShowcaseNodes(nodes).findNodeByPath(path.replaceFirst('/', '')); + } + + void _onRouteChange() { + value = _nodeFromPath(router.state.fullPath ?? '', nodes); + } + + @override + void dispose() { + router.routerDelegate.removeListener(_onRouteChange); + super.dispose(); + } +} + +class _InheritedNodeNotifier extends InheritedNotifier { + const _InheritedNodeNotifier({ + required super.notifier, + required super.child, + required this.state, + }); + + final ActiveNodeProviderState state; +} diff --git a/core/ui_showcase/lib/src/widget/active_node_provider.dart b/core/ui_showcase/lib/src/widget/active_node_provider.dart deleted file mode 100644 index ec7316fa..00000000 --- a/core/ui_showcase/lib/src/widget/active_node_provider.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:ui_showcase/ui_showcase.dart'; - -class ActiveNodeProvider extends InheritedWidget { - const ActiveNodeProvider({ - required this.activeNode, - required super.child, - super.key, - }); - - final ShowcaseNode? activeNode; - - static ActiveNodeProvider? maybeOf(BuildContext context) { - return context.dependOnInheritedWidgetOfExactType(); - } - - static ActiveNodeProvider of(BuildContext context) { - final result = maybeOf(context); - assert(result != null, 'No ActiveNodeProvider found in context'); - - return result!; - } - - @override - bool updateShouldNotify(ActiveNodeProvider oldWidget) { - return activeNode != oldWidget.activeNode; - } -} diff --git a/core/ui_showcase/lib/src/widget/showcase_tree_view.dart b/core/ui_showcase/lib/src/widget/showcase_tree_view.dart index 3f966655..489a5508 100644 --- a/core/ui_showcase/lib/src/widget/showcase_tree_view.dart +++ b/core/ui_showcase/lib/src/widget/showcase_tree_view.dart @@ -38,6 +38,7 @@ class ShowcaseTreeView extends StatefulWidget { switchOutCurve: animationCurve, child: Icon( node.isExpanded ? Icons.folder_open : Icons.folder, + color: Theme.of(context).colorScheme.onSurfaceVariant, key: ValueKey(node.isExpanded), ), ), @@ -72,19 +73,57 @@ class _ShowcaseTreeViewState extends State { @override Widget build(BuildContext context) { - return TreeView( - tree: _nodes, - treeRowBuilder: (node) => const TreeRow(extent: FixedTreeRowExtent(32)), - treeNodeBuilder: (context, node, style) => ShowcaseTreeView._nodeBuilder( - context: context, - node: node, - animationStyle: style, - onNodeSelected: (node) => _onNodeSelected(context, node), + return DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + border: Border( + right: BorderSide( + color: Theme.of(context).colorScheme.outlineVariant, + ), + ), ), - toggleAnimationStyle: const AnimationStyle( - duration: Duration(milliseconds: 100), - curve: Curves.easeInOut, - reverseCurve: Curves.easeInOut, + child: TreeView( + tree: [ + TreeViewNode( + ShowcaseFolderNode( + name: 'Button', + children: widget.nodes, + ), + children: [ + // TODO: Find out how tree view node hacked hot reload + TreeViewNode( + ShowcasePreviewNode( + name: 'Variant 1', + widget: SizedBox(), + ), + ), + TreeViewNode( + ShowcasePreviewNode( + name: 'Variant 2', + widget: SizedBox(), + ), + ), + TreeViewNode( + ShowcasePreviewNode( + name: 'Variant 3', + widget: SizedBox(), + ), + ), + ], + ), + ], + treeRowBuilder: (node) => const TreeRow(extent: FixedTreeRowExtent(32)), + treeNodeBuilder: (context, node, style) => ShowcaseTreeView._nodeBuilder( + context: context, + node: node, + animationStyle: style, + onNodeSelected: (node) => _onNodeSelected(context, node), + ), + toggleAnimationStyle: const AnimationStyle( + duration: Duration(milliseconds: 100), + curve: Curves.easeInOut, + reverseCurve: Curves.easeInOut, + ), ), ); } diff --git a/core/ui_showcase/lib/src/widget/showcase_view.dart b/core/ui_showcase/lib/src/widget/showcase_view.dart new file mode 100644 index 00000000..0a2e0a25 --- /dev/null +++ b/core/ui_showcase/lib/src/widget/showcase_view.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:ui_showcase/ui_showcase.dart'; + +class ShowcaseView extends StatelessWidget { + const ShowcaseView({ + required this.nodes, + required this.navigator, + }); + + final List nodes; + final Widget navigator; + + @override + Widget build(BuildContext context) { + return ActiveNodeProvider( + nodes: nodes, + child: _ShowcaseView(nodes: nodes, navigator: navigator), + ); + } +} + +/// A view that displays a list of showcase nodes. +/// +/// See more: +/// - [ShowcaseMobile] +/// - [ShowcaseDesktop] +class _ShowcaseView extends StatefulWidget { + const _ShowcaseView({ + required this.nodes, + required this.navigator, + }); + + final List nodes; + final Widget navigator; + + @override + State<_ShowcaseView> createState() => _ShowcaseViewState(); +} + +class _ShowcaseViewState extends State<_ShowcaseView> { + void _onNodeSelected(ShowcaseNode node) { + ActiveNodeProvider.of(context, listen: false).updateActiveNode(node); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.sizeOf(context); + Widget child; + + if (size.width < 600) { + child = ShowcaseMobile( + navigator: widget.navigator, + nodes: widget.nodes, + onNodeSelected: _onNodeSelected, + ); + } else { + child = ShowcaseDesktop( + navigator: widget.navigator, + nodes: widget.nodes, + onNodeSelected: _onNodeSelected, + ); + } + + return child; + } +} + +class ShowcaseMobile extends StatelessWidget { + const ShowcaseMobile({ + required this.navigator, + required this.nodes, + required this.onNodeSelected, + super.key, + }); + + final List nodes; + final ValueChanged onNodeSelected; + final Widget navigator; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + body: navigator, + ); + } +} + +class ShowcaseDesktop extends StatelessWidget { + const ShowcaseDesktop({ + required this.navigator, + required this.nodes, + required this.onNodeSelected, + super.key, + }); + + final List nodes; + final ValueChanged onNodeSelected; + final Widget navigator; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + body: Row( + children: [ + SizedBox( + width: 200, + child: ShowcaseTreeView( + nodes: nodes, + onNodeSelected: onNodeSelected, + selectedNode: ActiveNodeProvider.of(context).activeNode, + ), + ), + Expanded(child: navigator), + ], + ), + ); + } +} diff --git a/core/ui_showcase/lib/ui_showcase.dart b/core/ui_showcase/lib/ui_showcase.dart index 76825df3..f24aee58 100644 --- a/core/ui_showcase/lib/ui_showcase.dart +++ b/core/ui_showcase/lib/ui_showcase.dart @@ -1,3 +1,6 @@ export 'src/core/showcase_node.dart'; -export 'src/widget/active_node_provider.dart'; +export 'src/core/showcase_nodes.dart'; +export 'src/routing/go_router.dart'; +export 'src/widget/active_node_notifier.dart'; export 'src/widget/showcase_tree_view.dart'; +export 'src/widget/showcase_view.dart'; diff --git a/core/ui_showcase/pubspec.yaml b/core/ui_showcase/pubspec.yaml index bdd5d7d8..a92f6518 100644 --- a/core/ui_showcase/pubspec.yaml +++ b/core/ui_showcase/pubspec.yaml @@ -1,7 +1,6 @@ name: ui_showcase description: "A new Flutter package project." version: 0.0.1 -homepage: environment: sdk: ^3.8.0 @@ -11,45 +10,9 @@ dependencies: flutter: sdk: flutter two_dimensional_scrollables: ^0.3.7 + go_router: ^16.0.0 + collection: ^1.19.1 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - - # To add assets to your package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/to/asset-from-package - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/to/font-from-package From c643c40fdc46554a580b7c6a057a6c61071f6145 Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Sat, 2 Aug 2025 10:21:18 +0200 Subject: [PATCH 05/22] Simplify structure --- core/ui_library/example/lib/main.dart | 14 +- .../lib/src/core/showcase_node.dart | 123 ++++-------------- .../lib/src/core/showcase_nodes.dart | 21 ++- .../lib/src/routing/go_router.dart | 2 + .../lib/src/widget/showcase_tree_view.dart | 30 +---- 5 files changed, 58 insertions(+), 132 deletions(-) diff --git a/core/ui_library/example/lib/main.dart b/core/ui_library/example/lib/main.dart index 17e5a41a..2fb28b94 100644 --- a/core/ui_library/example/lib/main.dart +++ b/core/ui_library/example/lib/main.dart @@ -7,17 +7,17 @@ void main() { } final showcaseNodes = [ - ShowcaseFolderNode( + ShowcaseNode( name: 'Button', children: [ - ShowcasePreviewNode(name: 'Variant 1', widget: const SomeButtonPreview()), - ShowcasePreviewNode(name: 'Variant 2', widget: const SomeButtonPreview()), - ShowcasePreviewNode(name: 'Variant 3', widget: const SomeButtonPreview()), - ShowcaseFolderNode( + ShowcaseNode(name: 'Variant 1', widget: const SomeButtonPreview()), + ShowcaseNode(name: 'Variant 2', widget: const SomeButtonPreview()), + ShowcaseNode(name: 'Variant 3', widget: const SomeButtonPreview()), + ShowcaseNode( name: 'Folder', children: [ - ShowcasePreviewNode(name: 'Variant 1', widget: const SomeButtonPreview()), - ShowcasePreviewNode(name: 'Variant 2', widget: const SomeButtonPreview()), + ShowcaseNode(name: 'Variant 1', widget: const SomeButtonPreview()), + ShowcaseNode(name: 'Variant 2', widget: const SomeButtonPreview()), ], ), ], diff --git a/core/ui_showcase/lib/src/core/showcase_node.dart b/core/ui_showcase/lib/src/core/showcase_node.dart index 58cfdce8..3bf31eab 100644 --- a/core/ui_showcase/lib/src/core/showcase_node.dart +++ b/core/ui_showcase/lib/src/core/showcase_node.dart @@ -1,24 +1,30 @@ import 'package:flutter/material.dart'; -abstract class ShowcaseNode { +class ShowcaseNode { + ShowcaseNode({ + required this.name, + this.description, + this.tags = const [], + this.category, + this.isDeprecated = false, + this.widget, + this.children = const [], + }); + // Private fields ShowcaseNode? _parent; int _depth = 0; - // Abstract getters (must be implemented by subclasses) - String get name; - - // Virtual getters (can be overridden by subclasses) - String? get description => null; - List get tags => const []; - String? get category => null; - bool get isDeprecated => false; - Widget? get widget => null; + // Public properties + final String name; + final String? description; + final List tags; + final String? category; + final bool isDeprecated; + final Widget? widget; + final List children; // Computed getters - /// Get all children (empty for leaf nodes) - List get children => const []; - /// Get parent node ShowcaseNode? get parent => _parent; @@ -49,10 +55,6 @@ abstract class ShowcaseNode { redepthChild(child); } - void dropChild(ShowcaseNode child) { - child._parent = null; - } - void redepthChild(ShowcaseNode child) { if (child._depth <= _depth) { child._depth = _depth + 1; @@ -60,11 +62,18 @@ abstract class ShowcaseNode { } } - @protected - void redepthChildren() {} + void redepthChildren() { + for (final child in children) { + redepthChild(child); + } + } void visitChildren(void Function(ShowcaseNode child) visitor) { children.forEach(visitor); + + for (final child in children) { + child.visitChildren(visitor); + } } // Navigation and search methods @@ -102,79 +111,3 @@ abstract class ShowcaseNode { return breadcrumbs; } } - -class ShowcaseFolderNode extends ShowcaseNode { - ShowcaseFolderNode({ - required this.name, - required this.children, - this.description, - this.tags = const [], - this.category, - this.isDeprecated = false, - }); - - @override - final String name; - - @override - final String? description; - - @override - final String? category; - - @override - final List tags; - - @override - final bool isDeprecated; - - @override - final List children; - - // Override methods - @override - void redepthChildren() { - for (final child in children) { - redepthChild(child); - } - } - - @override - void visitChildren(void Function(ShowcaseNode child) visitor) { - children.forEach(visitor); - - for (final child in children) { - child.visitChildren(visitor); - } - } -} - -class ShowcasePreviewNode extends ShowcaseNode { - ShowcasePreviewNode({ - required this.name, - required this.widget, - this.description, - this.tags = const [], - this.category, - this.isDeprecated = false, - }); - - // Override properties - @override - final String name; - - @override - final String? description; - - @override - final String? category; - - @override - final List tags; - - @override - final bool isDeprecated; - - @override - final Widget widget; -} diff --git a/core/ui_showcase/lib/src/core/showcase_nodes.dart b/core/ui_showcase/lib/src/core/showcase_nodes.dart index 8206f070..718e06f6 100644 --- a/core/ui_showcase/lib/src/core/showcase_nodes.dart +++ b/core/ui_showcase/lib/src/core/showcase_nodes.dart @@ -24,7 +24,6 @@ extension type ShowcaseNodes(List nodes) { ShowcaseNode? found; node.visitChildren((child) { - print('child.fullPath: ${child.fullPath} path: $path'); if (child.fullPath == path) { found = child; } @@ -35,4 +34,24 @@ extension type ShowcaseNodes(List nodes) { return null; } + + void assignParents({ + int depth = 0, + List? nodes, + ShowcaseNode? parent, + }) { + nodes ??= this.nodes; + + for (final node in nodes) { + parent?.adoptChild(node); + + if (node.children.isNotEmpty) { + assignParents( + depth: depth + 1, + nodes: node.children, + parent: node, + ); + } + } + } } diff --git a/core/ui_showcase/lib/src/routing/go_router.dart b/core/ui_showcase/lib/src/routing/go_router.dart index fe65bfbd..81407b3f 100644 --- a/core/ui_showcase/lib/src/routing/go_router.dart +++ b/core/ui_showcase/lib/src/routing/go_router.dart @@ -3,6 +3,8 @@ import 'package:go_router/go_router.dart'; import 'package:ui_showcase/ui_showcase.dart'; GoRouter createRouter(List nodes) { + ShowcaseNodes(nodes).assignParents(); + return GoRouter( debugLogDiagnostics: true, routes: [ diff --git a/core/ui_showcase/lib/src/widget/showcase_tree_view.dart b/core/ui_showcase/lib/src/widget/showcase_tree_view.dart index 489a5508..e98bbd79 100644 --- a/core/ui_showcase/lib/src/widget/showcase_tree_view.dart +++ b/core/ui_showcase/lib/src/widget/showcase_tree_view.dart @@ -83,35 +83,7 @@ class _ShowcaseTreeViewState extends State { ), ), child: TreeView( - tree: [ - TreeViewNode( - ShowcaseFolderNode( - name: 'Button', - children: widget.nodes, - ), - children: [ - // TODO: Find out how tree view node hacked hot reload - TreeViewNode( - ShowcasePreviewNode( - name: 'Variant 1', - widget: SizedBox(), - ), - ), - TreeViewNode( - ShowcasePreviewNode( - name: 'Variant 2', - widget: SizedBox(), - ), - ), - TreeViewNode( - ShowcasePreviewNode( - name: 'Variant 3', - widget: SizedBox(), - ), - ), - ], - ), - ], + tree: _nodes, treeRowBuilder: (node) => const TreeRow(extent: FixedTreeRowExtent(32)), treeNodeBuilder: (context, node, style) => ShowcaseTreeView._nodeBuilder( context: context, From dddd60cc0001a8df29d1905058f933833b049218 Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Sat, 2 Aug 2025 11:05:31 +0200 Subject: [PATCH 06/22] Refactor routing and enhance button preview functionality - Updated `pubspec.yaml` to include `go_router` dependency. - Refactored routing logic in `go_router.dart` to use `GoRouter` directly. - Simplified `SomeButtonPreview` by removing unnecessary app bar and streamlining the button display. - Enhanced `ShowcaseDesktop` to include active node metadata and breadcrumbs for better navigation context. --- core/ui_library/example/lib/main.dart | 14 ++-- .../lib/src/previews/button_preview.dart | 14 ++-- core/ui_library/example/pubspec.lock | 2 +- core/ui_library/example/pubspec.yaml | 1 + .../lib/src/routing/go_router.dart | 29 ++++---- .../lib/src/widget/showcase_tree_view.dart | 8 ++- .../lib/src/widget/showcase_view.dart | 71 ++++++++++++++++++- 7 files changed, 99 insertions(+), 40 deletions(-) diff --git a/core/ui_library/example/lib/main.dart b/core/ui_library/example/lib/main.dart index 2fb28b94..6f2d80c0 100644 --- a/core/ui_library/example/lib/main.dart +++ b/core/ui_library/example/lib/main.dart @@ -1,5 +1,6 @@ import 'package:example/src/previews/button_preview.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:ui_showcase/ui_showcase.dart'; void main() { @@ -13,13 +14,6 @@ final showcaseNodes = [ ShowcaseNode(name: 'Variant 1', widget: const SomeButtonPreview()), ShowcaseNode(name: 'Variant 2', widget: const SomeButtonPreview()), ShowcaseNode(name: 'Variant 3', widget: const SomeButtonPreview()), - ShowcaseNode( - name: 'Folder', - children: [ - ShowcaseNode(name: 'Variant 1', widget: const SomeButtonPreview()), - ShowcaseNode(name: 'Variant 2', widget: const SomeButtonPreview()), - ], - ), ], ), ]; @@ -37,7 +31,11 @@ class _MyAppState extends State { @override void initState() { super.initState(); - router = createRouter(showcaseNodes); + router = GoRouter( + routes: [ + createRootShowcaseRoute(showcaseNodes), + ], + ); } @override diff --git a/core/ui_library/example/lib/src/previews/button_preview.dart b/core/ui_library/example/lib/src/previews/button_preview.dart index 4e4ad9e1..8a38966c 100644 --- a/core/ui_library/example/lib/src/previews/button_preview.dart +++ b/core/ui_library/example/lib/src/previews/button_preview.dart @@ -1,20 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:ui_showcase/ui_showcase.dart'; class SomeButtonPreview extends StatelessWidget { const SomeButtonPreview({super.key}); @override Widget build(BuildContext context) { - final activeNode = ActiveNodeProvider.of(context).activeNode!; - - return Scaffold( - appBar: AppBar(title: Text(activeNode.fullPath)), - body: Center( - child: ElevatedButton( - onPressed: () {}, - child: const Text('Click me'), - ), + return Center( + child: ElevatedButton( + onPressed: () {}, + child: const Text('Click me'), ), ); } diff --git a/core/ui_library/example/pubspec.lock b/core/ui_library/example/pubspec.lock index 8621db2a..ae9810ee 100644 --- a/core/ui_library/example/pubspec.lock +++ b/core/ui_library/example/pubspec.lock @@ -65,7 +65,7 @@ packages: source: sdk version: "0.0.0" go_router: - dependency: transitive + dependency: "direct main" description: name: go_router sha256: c489908a54ce2131f1d1b7cc631af9c1a06fac5ca7c449e959192089f9489431 diff --git a/core/ui_library/example/pubspec.yaml b/core/ui_library/example/pubspec.yaml index cc660969..0d1132df 100644 --- a/core/ui_library/example/pubspec.yaml +++ b/core/ui_library/example/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: sdk: flutter ui_showcase: path: ../../ui_showcase + go_router: ^16.0.0 dev_dependencies: flutter_test: diff --git a/core/ui_showcase/lib/src/routing/go_router.dart b/core/ui_showcase/lib/src/routing/go_router.dart index 81407b3f..dff01ded 100644 --- a/core/ui_showcase/lib/src/routing/go_router.dart +++ b/core/ui_showcase/lib/src/routing/go_router.dart @@ -2,34 +2,29 @@ import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:ui_showcase/ui_showcase.dart'; -GoRouter createRouter(List nodes) { +RouteBase createRootShowcaseRoute(List nodes) { ShowcaseNodes(nodes).assignParents(); - return GoRouter( - debugLogDiagnostics: true, + return ShellRoute( routes: [ - ShellRoute( - routes: [ - GoRoute( - path: '/', - builder: (context, state) => const SizedBox.shrink(), - routes: createRoutes(nodes), - ), - ], - builder: (context, state, child) => ShowcaseView( - nodes: nodes, - navigator: child, - ), + GoRoute( + path: '/', + builder: (context, state) => const SizedBox.shrink(), + routes: _createRoutes(nodes), ), ], + builder: (context, state, child) => ShowcaseView( + nodes: nodes, + navigator: child, + ), ); } -List createRoutes(List nodes) { +List _createRoutes(List nodes) { final routes = []; for (final node in nodes) { - final childrenRoutes = node.children.isEmpty ? [] : createRoutes(node.children); + final childrenRoutes = node.children.isEmpty ? [] : _createRoutes(node.children); routes.add( GoRoute( diff --git a/core/ui_showcase/lib/src/widget/showcase_tree_view.dart b/core/ui_showcase/lib/src/widget/showcase_tree_view.dart index e98bbd79..c30b82db 100644 --- a/core/ui_showcase/lib/src/widget/showcase_tree_view.dart +++ b/core/ui_showcase/lib/src/widget/showcase_tree_view.dart @@ -55,6 +55,7 @@ class ShowcaseTreeView extends StatefulWidget { class _ShowcaseTreeViewState extends State { late final _nodes = _buildTree(widget.nodes); + final _treeViewController = TreeViewController(); List> _buildTree(List nodes) { return nodes @@ -62,9 +63,9 @@ class _ShowcaseTreeViewState extends State { .toList(growable: false); } - void _onNodeSelected(BuildContext context, TreeViewNode node) { + void _onNodeSelected(TreeViewNode node) { if (node.children.isNotEmpty) { - TreeViewController.of(context).toggleNode(node); + _treeViewController.toggleNode(node); return; } @@ -84,12 +85,13 @@ class _ShowcaseTreeViewState extends State { ), child: TreeView( tree: _nodes, + controller: _treeViewController, treeRowBuilder: (node) => const TreeRow(extent: FixedTreeRowExtent(32)), treeNodeBuilder: (context, node, style) => ShowcaseTreeView._nodeBuilder( context: context, node: node, animationStyle: style, - onNodeSelected: (node) => _onNodeSelected(context, node), + onNodeSelected: _onNodeSelected, ), toggleAnimationStyle: const AnimationStyle( duration: Duration(milliseconds: 100), diff --git a/core/ui_showcase/lib/src/widget/showcase_view.dart b/core/ui_showcase/lib/src/widget/showcase_view.dart index 0a2e0a25..9cc1e54d 100644 --- a/core/ui_showcase/lib/src/widget/showcase_view.dart +++ b/core/ui_showcase/lib/src/widget/showcase_view.dart @@ -100,6 +100,8 @@ class ShowcaseDesktop extends StatelessWidget { @override Widget build(BuildContext context) { + final activeNode = ActiveNodeProvider.of(context).activeNode; + return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, body: Row( @@ -112,9 +114,76 @@ class ShowcaseDesktop extends StatelessWidget { selectedNode: ActiveNodeProvider.of(context).activeNode, ), ), - Expanded(child: navigator), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (activeNode != null) + _ActiveNodeMetadata( + node: activeNode, + onBreadcrumbTap: onNodeSelected, + ), + Expanded(child: navigator), + ], + ), + ), + ], + ), + ); + } +} + +class _ActiveNodeMetadata extends StatelessWidget { + const _ActiveNodeMetadata({ + required this.node, + required this.onBreadcrumbTap, + }); + + final ShowcaseNode node; + final ValueChanged onBreadcrumbTap; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Breadcrumbs(node: node, onBreadcrumbTap: onBreadcrumbTap), + if (node.description case final description?) Text(description), ], ), ); } } + +class _Breadcrumbs extends StatelessWidget { + const _Breadcrumbs({ + required this.node, + required this.onBreadcrumbTap, + }); + + final ShowcaseNode node; + final ValueChanged onBreadcrumbTap; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + final colorScheme = Theme.of(context).colorScheme; + final textStyle = textTheme.bodyMedium?.copyWith(color: colorScheme.primary); + + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (final parent in node.getBreadcrumbs()) ...[ + TextButton( + onPressed: () => onBreadcrumbTap(parent), + child: Text(parent.path, style: textStyle), + ), + if (parent != node) const Text('>'), + ], + ], + ); + } +} From 409c2f77eeeab7465711e8926c2839c2a7d42517 Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Sat, 2 Aug 2025 12:13:19 +0200 Subject: [PATCH 07/22] Enhance ShowcaseTreeView with selection state and expanded node logic - Added `isSelected` parameter to `ShowcaseTreeView` to conditionally style node text based on selection. - Improved tree node expansion logic to reflect the selected node's state, enhancing user interaction and visual feedback. --- .../lib/src/widget/showcase_tree_view.dart | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/core/ui_showcase/lib/src/widget/showcase_tree_view.dart b/core/ui_showcase/lib/src/widget/showcase_tree_view.dart index c30b82db..5bed0ea1 100644 --- a/core/ui_showcase/lib/src/widget/showcase_tree_view.dart +++ b/core/ui_showcase/lib/src/widget/showcase_tree_view.dart @@ -19,6 +19,7 @@ class ShowcaseTreeView extends StatefulWidget { required TreeViewNode node, required AnimationStyle animationStyle, required ValueChanged> onNodeSelected, + required bool isSelected, }) { final animationDuration = animationStyle.duration ?? TreeView.defaultAnimationDuration; final animationCurve = animationStyle.curve ?? TreeView.defaultAnimationCurve; @@ -42,7 +43,10 @@ class ShowcaseTreeView extends StatefulWidget { key: ValueKey(node.isExpanded), ), ), - Text(node.content.name), + Text( + node.content.name, + style: TextStyle(color: isSelected ? Theme.of(context).colorScheme.primary : null), + ), ], ), ), @@ -58,9 +62,25 @@ class _ShowcaseTreeViewState extends State { final _treeViewController = TreeViewController(); List> _buildTree(List nodes) { - return nodes - .map((node) => TreeViewNode(node, children: _buildTree(node.children))) - .toList(growable: false); + final treeViewNodes = >[]; + + for (final node in nodes) { + var isExpanded = false; + + if (widget.selectedNode case final selectedNode?) { + isExpanded = selectedNode.findByName(selectedNode.name) != null; + } + + final treeViewNode = TreeViewNode( + node, + children: _buildTree(node.children), + expanded: isExpanded, + ); + + treeViewNodes.add(treeViewNode); + } + + return treeViewNodes; } void _onNodeSelected(TreeViewNode node) { @@ -92,6 +112,7 @@ class _ShowcaseTreeViewState extends State { node: node, animationStyle: style, onNodeSelected: _onNodeSelected, + isSelected: node.content == widget.selectedNode, ), toggleAnimationStyle: const AnimationStyle( duration: Duration(milliseconds: 100), From 9cb3c5678fa76d468b2915e807be07313b0ce40d Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Sat, 2 Aug 2025 16:24:40 +0200 Subject: [PATCH 08/22] Refactor ShowcaseTreeView for improved node handling and UI interaction - Introduced a new `_TreeNodeWidget` to encapsulate node rendering logic, enhancing code organization. - Updated node selection handling with haptic feedback and improved visual feedback for selected and hovered states. - Simplified tree node building logic and ensured consistent animation styles across node interactions. - Enhanced the ShowcaseDesktop to streamline active node metadata display. --- .../lib/src/widget/showcase_tree_view.dart | 294 +++++++++++++----- .../lib/src/widget/showcase_view.dart | 21 +- 2 files changed, 225 insertions(+), 90 deletions(-) diff --git a/core/ui_showcase/lib/src/widget/showcase_tree_view.dart b/core/ui_showcase/lib/src/widget/showcase_tree_view.dart index 5bed0ea1..11a0e53a 100644 --- a/core/ui_showcase/lib/src/widget/showcase_tree_view.dart +++ b/core/ui_showcase/lib/src/widget/showcase_tree_view.dart @@ -1,7 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart'; import 'package:ui_showcase/ui_showcase.dart'; +/// A tree view widget for displaying showcase nodes in a hierarchical structure. +/// +/// This widget provides an intuitive interface for navigating through UI components +/// with features like expandable folders, selection highlighting, and smooth animations. class ShowcaseTreeView extends StatefulWidget { const ShowcaseTreeView({ required this.nodes, @@ -10,116 +15,257 @@ class ShowcaseTreeView extends StatefulWidget { super.key, }); + /// The root nodes to display in the tree final List nodes; + + /// The currently selected node, if any final ShowcaseNode? selectedNode; + + /// Callback fired when a node is selected final ValueChanged? onNodeSelected; - static Widget _nodeBuilder({ - required BuildContext context, - required TreeViewNode node, - required AnimationStyle animationStyle, - required ValueChanged> onNodeSelected, - required bool isSelected, - }) { - final animationDuration = animationStyle.duration ?? TreeView.defaultAnimationDuration; - final animationCurve = animationStyle.curve ?? TreeView.defaultAnimationCurve; - - return Padding( - padding: const EdgeInsets.only(left: 8), - child: GestureDetector( - onTap: () => onNodeSelected(node), - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 4, - children: [ - if (node.children.isNotEmpty) - AnimatedSwitcher( - duration: animationDuration, - switchInCurve: animationCurve, - switchOutCurve: animationCurve, - child: Icon( - node.isExpanded ? Icons.folder_open : Icons.folder, - color: Theme.of(context).colorScheme.onSurfaceVariant, - key: ValueKey(node.isExpanded), - ), - ), - Text( - node.content.name, - style: TextStyle(color: isSelected ? Theme.of(context).colorScheme.primary : null), - ), - ], - ), - ), - ); - } + static const double _nodeHeight = 36.0; + static const double _nodeIndentation = 8.0; + static const double _iconSize = 18.0; + static const Duration _animationDuration = Duration(milliseconds: 150); + static const Curve _animationCurve = Curves.easeInOutCubic; @override State createState() => _ShowcaseTreeViewState(); } class _ShowcaseTreeViewState extends State { - late final _nodes = _buildTree(widget.nodes); - final _treeViewController = TreeViewController(); + late final List> _treeNodes; + final TreeViewController _treeViewController = TreeViewController(); - List> _buildTree(List nodes) { - final treeViewNodes = >[]; + @override + void initState() { + super.initState(); + _treeNodes = _buildTreeNodes(widget.nodes); + } - for (final node in nodes) { - var isExpanded = false; + List> _buildTreeNodes(List nodes) { + return nodes.map(_createTreeViewNode).toList(); + } - if (widget.selectedNode case final selectedNode?) { - isExpanded = selectedNode.findByName(selectedNode.name) != null; - } + TreeViewNode _createTreeViewNode(ShowcaseNode node) { + final isExpanded = _shouldNodeBeExpanded(node); - final treeViewNode = TreeViewNode( - node, - children: _buildTree(node.children), - expanded: isExpanded, - ); + return TreeViewNode( + node, + children: _buildTreeNodes(node.children), + expanded: isExpanded, + ); + } - treeViewNodes.add(treeViewNode); - } + bool _shouldNodeBeExpanded(ShowcaseNode node) { + final selectedNode = widget.selectedNode; + if (selectedNode == null) return false; - return treeViewNodes; + return node.findByName(selectedNode.name) != null; } - void _onNodeSelected(TreeViewNode node) { - if (node.children.isNotEmpty) { - _treeViewController.toggleNode(node); - return; - } + void _handleNodeSelection(TreeViewNode treeNode) { + HapticFeedback.selectionClick(); - widget.onNodeSelected?.call(node.content); + if (treeNode.children.isNotEmpty) { + _treeViewController.toggleNode(treeNode); + } else { + widget.onNodeSelected?.call(treeNode.content); + } } @override Widget build(BuildContext context) { + final theme = Theme.of(context); + return DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, + color: theme.colorScheme.surfaceContainer, border: Border( - right: BorderSide( - color: Theme.of(context).colorScheme.outlineVariant, - ), + right: BorderSide(color: theme.colorScheme.outlineVariant), ), ), child: TreeView( - tree: _nodes, + tree: _treeNodes, controller: _treeViewController, - treeRowBuilder: (node) => const TreeRow(extent: FixedTreeRowExtent(32)), - treeNodeBuilder: (context, node, style) => ShowcaseTreeView._nodeBuilder( - context: context, + indentation: TreeViewIndentationType.none, + treeRowBuilder: (_) => const TreeRow( + extent: FixedTreeRowExtent(ShowcaseTreeView._nodeHeight), + ), + treeNodeBuilder: (context, node, animationStyle) => _TreeNodeWidget( node: node, - animationStyle: style, - onNodeSelected: _onNodeSelected, + animationStyle: animationStyle, + onNodeSelected: _handleNodeSelection, isSelected: node.content == widget.selectedNode, ), toggleAnimationStyle: const AnimationStyle( - duration: Duration(milliseconds: 100), - curve: Curves.easeInOut, - reverseCurve: Curves.easeInOut, + duration: ShowcaseTreeView._animationDuration, + curve: ShowcaseTreeView._animationCurve, + reverseCurve: ShowcaseTreeView._animationCurve, + ), + ), + ); + } +} + +class _TreeNodeWidget extends StatefulWidget { + const _TreeNodeWidget({ + required this.node, + required this.animationStyle, + required this.onNodeSelected, + required this.isSelected, + }); + + final TreeViewNode node; + final AnimationStyle animationStyle; + final ValueChanged> onNodeSelected; + final bool isSelected; + + @override + State<_TreeNodeWidget> createState() => _TreeNodeWidgetState(); +} + +class _TreeNodeWidgetState extends State<_TreeNodeWidget> { + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final node = widget.node; + final showcaseNode = node.content; + final hasChildren = node.children.isNotEmpty; + + final backgroundColor = _getBackgroundColor(theme); + final textColor = _getTextColor(theme); + final iconColor = _getIconColor(theme); + + return MouseRegion( + onEnter: (_) => _setHovered(true), + onExit: (_) => _setHovered(false), + child: GestureDetector( + onTap: () => widget.onNodeSelected(node), + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 200), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + child: DecoratedBox( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(6), + border: widget.isSelected + ? Border.all(color: theme.colorScheme.primary.withValues(alpha: 0.3)) + : null, + ), + child: Padding( + padding: EdgeInsets.only( + left: ShowcaseTreeView._nodeIndentation * (showcaseNode.depth + 1), + top: 6, + bottom: 6, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (hasChildren) ...[ + _buildExpansionIcon(iconColor), + const SizedBox(width: 8), + ] else ...[ + Padding( + padding: const EdgeInsets.only(right: 8), + child: SizedBox( + width: ShowcaseTreeView._iconSize, + height: ShowcaseTreeView._iconSize, + child: DecoratedBox( + decoration: BoxDecoration( + color: iconColor.withValues(alpha: 0.2), + shape: BoxShape.circle, + ), + child: Icon( + Icons.album_rounded, + size: 12, + color: iconColor, + ), + ), + ), + ), + ], + + Flexible( + child: Text( + showcaseNode.name, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodyMedium?.copyWith( + color: textColor, + height: 1.2, + ), + ), + ), + ], + ), + ), + ), + ), ), ), ); } + + Widget _buildExpansionIcon(Color iconColor) { + final animationDuration = widget.animationStyle.duration ?? ShowcaseTreeView._animationDuration; + final animationCurve = widget.animationStyle.curve ?? ShowcaseTreeView._animationCurve; + + return AnimatedSwitcher( + duration: animationDuration, + switchInCurve: animationCurve, + switchOutCurve: animationCurve, + child: Icon( + widget.node.isExpanded ? Icons.folder_open_outlined : Icons.folder_outlined, + color: iconColor, + size: ShowcaseTreeView._iconSize, + key: ValueKey(widget.node.isExpanded), + ), + ); + } + + Color _getBackgroundColor(ThemeData theme) { + if (widget.isSelected) { + return theme.colorScheme.primaryContainer.withValues(alpha: 0.3); + } + + if (_isHovered) { + return theme.colorScheme.onSurface.withValues(alpha: 0.05); + } + + return Colors.transparent; + } + + Color _getTextColor(ThemeData theme) { + if (widget.isSelected) { + return theme.colorScheme.onPrimaryContainer; + } + + if (widget.node.content.isDeprecated) { + return theme.colorScheme.onSurface.withValues(alpha: 0.6); + } + + return theme.colorScheme.onSurface; + } + + Color _getIconColor(ThemeData theme) { + if (widget.isSelected) { + return theme.colorScheme.primary; + } + if (_isHovered) { + return theme.colorScheme.onSurface.withValues(alpha: 0.8); + } + return theme.colorScheme.onSurfaceVariant; + } + + void _setHovered(bool hovered) { + if (_isHovered != hovered) { + setState(() { + _isHovered = hovered; + }); + } + } } diff --git a/core/ui_showcase/lib/src/widget/showcase_view.dart b/core/ui_showcase/lib/src/widget/showcase_view.dart index 9cc1e54d..9eb139af 100644 --- a/core/ui_showcase/lib/src/widget/showcase_view.dart +++ b/core/ui_showcase/lib/src/widget/showcase_view.dart @@ -118,11 +118,7 @@ class ShowcaseDesktop extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (activeNode != null) - _ActiveNodeMetadata( - node: activeNode, - onBreadcrumbTap: onNodeSelected, - ), + if (activeNode != null) _ActiveNodeMetadata(node: activeNode), Expanded(child: navigator), ], ), @@ -136,11 +132,9 @@ class ShowcaseDesktop extends StatelessWidget { class _ActiveNodeMetadata extends StatelessWidget { const _ActiveNodeMetadata({ required this.node, - required this.onBreadcrumbTap, }); final ShowcaseNode node; - final ValueChanged onBreadcrumbTap; @override Widget build(BuildContext context) { @@ -149,7 +143,7 @@ class _ActiveNodeMetadata extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _Breadcrumbs(node: node, onBreadcrumbTap: onBreadcrumbTap), + _Breadcrumbs(node: node), if (node.description case final description?) Text(description), ], ), @@ -160,28 +154,23 @@ class _ActiveNodeMetadata extends StatelessWidget { class _Breadcrumbs extends StatelessWidget { const _Breadcrumbs({ required this.node, - required this.onBreadcrumbTap, }); final ShowcaseNode node; - final ValueChanged onBreadcrumbTap; @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; final colorScheme = Theme.of(context).colorScheme; - final textStyle = textTheme.bodyMedium?.copyWith(color: colorScheme.primary); + final textStyle = textTheme.bodyMedium?.copyWith(color: colorScheme.secondary); return Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ for (final parent in node.getBreadcrumbs()) ...[ - TextButton( - onPressed: () => onBreadcrumbTap(parent), - child: Text(parent.path, style: textStyle), - ), - if (parent != node) const Text('>'), + Text(parent.path, style: textStyle), + if (parent != node) Text('/', style: textStyle), ], ], ); From 668e71f217491431e9dab3350c0bae48401096ad Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Sat, 2 Aug 2025 16:54:21 +0200 Subject: [PATCH 09/22] Update showcase view for mobile --- .cursor/rules/flutter.mdc | 8 ++++ .../lib/src/widget/showcase_tree_view.dart | 13 ++++-- .../lib/src/widget/showcase_view.dart | 42 +++++++++++++++++-- 3 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 .cursor/rules/flutter.mdc diff --git a/.cursor/rules/flutter.mdc b/.cursor/rules/flutter.mdc new file mode 100644 index 00000000..7b7ec160 --- /dev/null +++ b/.cursor/rules/flutter.mdc @@ -0,0 +1,8 @@ +--- +globs: app/**/*.dart,core/**/*.dart +alwaysApply: false +--- + +## Widgets + +- Use SizedBox, DecoratedBox, Padding, etc. instead of Container. diff --git a/core/ui_showcase/lib/src/widget/showcase_tree_view.dart b/core/ui_showcase/lib/src/widget/showcase_tree_view.dart index 11a0e53a..8eb774ee 100644 --- a/core/ui_showcase/lib/src/widget/showcase_tree_view.dart +++ b/core/ui_showcase/lib/src/widget/showcase_tree_view.dart @@ -12,9 +12,12 @@ class ShowcaseTreeView extends StatefulWidget { required this.nodes, this.selectedNode, this.onNodeSelected, + this.width, super.key, }); + final double? width; + /// The root nodes to display in the tree final List nodes; @@ -24,7 +27,7 @@ class ShowcaseTreeView extends StatefulWidget { /// Callback fired when a node is selected final ValueChanged? onNodeSelected; - static const double _nodeHeight = 36.0; + static const double _nodeHeight = 40.0; static const double _nodeIndentation = 8.0; static const double _iconSize = 18.0; static const Duration _animationDuration = Duration(milliseconds: 150); @@ -98,6 +101,7 @@ class _ShowcaseTreeViewState extends State { animationStyle: animationStyle, onNodeSelected: _handleNodeSelection, isSelected: node.content == widget.selectedNode, + width: widget.width, ), toggleAnimationStyle: const AnimationStyle( duration: ShowcaseTreeView._animationDuration, @@ -115,12 +119,14 @@ class _TreeNodeWidget extends StatefulWidget { required this.animationStyle, required this.onNodeSelected, required this.isSelected, + this.width, }); final TreeViewNode node; final AnimationStyle animationStyle; final ValueChanged> onNodeSelected; final bool isSelected; + final double? width; @override State<_TreeNodeWidget> createState() => _TreeNodeWidgetState(); @@ -141,14 +147,15 @@ class _TreeNodeWidgetState extends State<_TreeNodeWidget> { final iconColor = _getIconColor(theme); return MouseRegion( + cursor: SystemMouseCursors.click, onEnter: (_) => _setHovered(true), onExit: (_) => _setHovered(false), child: GestureDetector( onTap: () => widget.onNodeSelected(node), child: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 200), + constraints: BoxConstraints(minWidth: widget.width ?? 200), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + padding: const EdgeInsets.only(left: 4, right: 4, top: 4), child: DecoratedBox( decoration: BoxDecoration( color: backgroundColor, diff --git a/core/ui_showcase/lib/src/widget/showcase_view.dart b/core/ui_showcase/lib/src/widget/showcase_view.dart index 9eb139af..e81bdb5c 100644 --- a/core/ui_showcase/lib/src/widget/showcase_view.dart +++ b/core/ui_showcase/lib/src/widget/showcase_view.dart @@ -79,9 +79,45 @@ class ShowcaseMobile extends StatelessWidget { @override Widget build(BuildContext context) { + final activeNode = ActiveNodeProvider.of(context).activeNode; + return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, - body: navigator, + drawer: Drawer( + child: ShowcaseTreeView( + nodes: nodes, + selectedNode: activeNode, + width: 304, + onNodeSelected: (value) { + onNodeSelected(value); + Navigator.pop(context); + }, + ), + ), + body: Builder( + builder: (context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.menu_rounded), + onPressed: () { + Scaffold.of(context).openDrawer(); + }, + ), + if (activeNode case final node?) _ActiveNodeMetadata(node: node), + ], + ), + ), + Expanded(child: navigator), + ], + ); + }, + ), ); } } @@ -130,9 +166,7 @@ class ShowcaseDesktop extends StatelessWidget { } class _ActiveNodeMetadata extends StatelessWidget { - const _ActiveNodeMetadata({ - required this.node, - }); + const _ActiveNodeMetadata({required this.node}); final ShowcaseNode node; From 69bb8f1c463aa60c4055a82d4e12ac7bd36d90d1 Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Sat, 2 Aug 2025 19:32:55 +0200 Subject: [PATCH 10/22] Enhance showcase functionality with new components and previews - Added `SomeCardPreview` to showcase card components alongside buttons. - Refactored `SomeButtonPreview` to utilize `ComponentPreview` for better input handling. - Introduced new input widgets: `StringInput` and `IntegerInput` for dynamic user interaction. - Implemented `ShowcaseTreeView` and `ShowcaseView` for improved navigation and display of showcase nodes. - Added `ActiveNodeMetadata` for contextual information about the selected node. - Updated exports in `ui_showcase.dart` to include new components and inputs. --- core/ui_library/example/lib/main.dart | 12 +-- .../lib/src/previews/button_preview.dart | 30 ++++-- .../lib/src/previews/card_preview.dart | 35 ++++++ .../lib/src/widget/inputs/integer_input.dart | 55 ++++++++++ .../lib/src/widget/inputs/string_input.dart | 41 +++++++ .../widget/showcase/active_node_metadata.dart | 48 +++++++++ .../{ => showcase}/active_node_notifier.dart | 0 .../widget/showcase/component_preview.dart | 101 ++++++++++++++++++ .../{ => showcase}/showcase_tree_view.dart | 0 .../widget/{ => showcase}/showcase_view.dart | 61 +---------- core/ui_showcase/lib/ui_showcase.dart | 9 +- 11 files changed, 317 insertions(+), 75 deletions(-) create mode 100644 core/ui_library/example/lib/src/previews/card_preview.dart create mode 100644 core/ui_showcase/lib/src/widget/inputs/integer_input.dart create mode 100644 core/ui_showcase/lib/src/widget/inputs/string_input.dart create mode 100644 core/ui_showcase/lib/src/widget/showcase/active_node_metadata.dart rename core/ui_showcase/lib/src/widget/{ => showcase}/active_node_notifier.dart (100%) create mode 100644 core/ui_showcase/lib/src/widget/showcase/component_preview.dart rename core/ui_showcase/lib/src/widget/{ => showcase}/showcase_tree_view.dart (100%) rename core/ui_showcase/lib/src/widget/{ => showcase}/showcase_view.dart (69%) diff --git a/core/ui_library/example/lib/main.dart b/core/ui_library/example/lib/main.dart index 6f2d80c0..837c61af 100644 --- a/core/ui_library/example/lib/main.dart +++ b/core/ui_library/example/lib/main.dart @@ -1,4 +1,5 @@ import 'package:example/src/previews/button_preview.dart'; +import 'package:example/src/previews/card_preview.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:ui_showcase/ui_showcase.dart'; @@ -8,14 +9,8 @@ void main() { } final showcaseNodes = [ - ShowcaseNode( - name: 'Button', - children: [ - ShowcaseNode(name: 'Variant 1', widget: const SomeButtonPreview()), - ShowcaseNode(name: 'Variant 2', widget: const SomeButtonPreview()), - ShowcaseNode(name: 'Variant 3', widget: const SomeButtonPreview()), - ], - ), + ShowcaseNode(name: 'Button', widget: const SomeButtonPreview()), + ShowcaseNode(name: 'Card', widget: const SomeCardPreview()), ]; class MyApp extends StatefulWidget { @@ -47,6 +42,7 @@ class _MyAppState extends State { routerConfig: router, theme: lightTheme, darkTheme: darkTheme, + debugShowCheckedModeBanner: false, ); } } diff --git a/core/ui_library/example/lib/src/previews/button_preview.dart b/core/ui_library/example/lib/src/previews/button_preview.dart index 8a38966c..2844fc11 100644 --- a/core/ui_library/example/lib/src/previews/button_preview.dart +++ b/core/ui_library/example/lib/src/previews/button_preview.dart @@ -1,15 +1,33 @@ import 'package:flutter/material.dart'; +import 'package:ui_showcase/ui_showcase.dart'; -class SomeButtonPreview extends StatelessWidget { +class SomeButtonPreview extends StatefulWidget { const SomeButtonPreview({super.key}); + @override + State createState() => _SomeButtonPreviewState(); +} + +class _SomeButtonPreviewState extends State { + final _controller = TextEditingController(text: 'Click Me!'); + @override Widget build(BuildContext context) { - return Center( - child: ElevatedButton( - onPressed: () {}, - child: const Text('Click me'), - ), + return ComponentPreview( + listenables: [_controller], + inputs: [ + StringInput( + label: 'Label', + description: 'The label of the button', + controller: _controller, + ), + ], + builder: (context) { + return ElevatedButton( + onPressed: () {}, + child: Text(_controller.text), + ); + }, ); } } diff --git a/core/ui_library/example/lib/src/previews/card_preview.dart b/core/ui_library/example/lib/src/previews/card_preview.dart new file mode 100644 index 00000000..f1fe5e68 --- /dev/null +++ b/core/ui_library/example/lib/src/previews/card_preview.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:ui_showcase/ui_showcase.dart'; + +class SomeCardPreview extends StatefulWidget { + const SomeCardPreview({super.key}); + + @override + State createState() => _SomeCardPreviewState(); +} + +class _SomeCardPreviewState extends State { + final _controller = TextEditingController(text: 'Card'); + + @override + Widget build(BuildContext context) { + return ComponentPreview( + listenables: [_controller], + inputs: [ + StringInput( + label: 'Label', + description: 'The label of the button', + controller: _controller, + ), + ], + builder: (context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(_controller.text), + ), + ); + }, + ); + } +} diff --git a/core/ui_showcase/lib/src/widget/inputs/integer_input.dart b/core/ui_showcase/lib/src/widget/inputs/integer_input.dart new file mode 100644 index 00000000..00b63c48 --- /dev/null +++ b/core/ui_showcase/lib/src/widget/inputs/integer_input.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +class IntegerInput extends StatelessWidget { + const IntegerInput({ + required this.notifier, + required this.label, + this.description, + this.min, + this.max, + }); + + final ValueNotifier notifier; + final String label; + final int? min; + final int? max; + final String? description; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final colorScheme = theme.colorScheme; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: textTheme.labelLarge?.copyWith(color: colorScheme.onSurface), + ), + ValueListenableBuilder( + valueListenable: notifier, + builder: (context, value, child) { + return SliderTheme( + data: const SliderThemeData(showValueIndicator: ShowValueIndicator.always), + child: Slider( + value: value.toDouble(), + min: min?.toDouble() ?? 0, + max: max?.toDouble() ?? 100, + label: value.toString(), + onChanged: (value) => notifier.value = value.toInt(), + ), + ); + }, + ), + if (description case final description?) + Text( + description, + style: textTheme.bodySmall?.copyWith(color: colorScheme.outline), + ), + ], + ); + } +} diff --git a/core/ui_showcase/lib/src/widget/inputs/string_input.dart b/core/ui_showcase/lib/src/widget/inputs/string_input.dart new file mode 100644 index 00000000..61b059b9 --- /dev/null +++ b/core/ui_showcase/lib/src/widget/inputs/string_input.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +class StringInput extends StatelessWidget { + const StringInput({ + required this.label, + required this.controller, + this.description, + this.hint, + }); + + final TextEditingController controller; + final String label; + final String? description; + final String? hint; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final colorScheme = theme.colorScheme; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: controller, + decoration: InputDecoration( + labelText: label, + hintText: hint, + ), + ), + if (description case final description?) + Text( + description, + style: textTheme.bodySmall?.copyWith(color: colorScheme.secondary), + ), + ], + ); + } +} diff --git a/core/ui_showcase/lib/src/widget/showcase/active_node_metadata.dart b/core/ui_showcase/lib/src/widget/showcase/active_node_metadata.dart new file mode 100644 index 00000000..5da1e8a9 --- /dev/null +++ b/core/ui_showcase/lib/src/widget/showcase/active_node_metadata.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:ui_showcase/ui_showcase.dart'; + +class ActiveNodeMetadata extends StatelessWidget { + const ActiveNodeMetadata({required this.node}); + + final ShowcaseNode node; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Breadcrumbs(node: node), + if (node.description case final description?) Text(description), + ], + ), + ); + } +} + +class Breadcrumbs extends StatelessWidget { + const Breadcrumbs({ + required this.node, + }); + + final ShowcaseNode node; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + final colorScheme = Theme.of(context).colorScheme; + final textStyle = textTheme.bodyMedium?.copyWith(color: colorScheme.secondary); + + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (final parent in node.getBreadcrumbs()) ...[ + Text(parent.path, style: textStyle), + if (parent != node) Text('/', style: textStyle), + ], + ], + ); + } +} diff --git a/core/ui_showcase/lib/src/widget/active_node_notifier.dart b/core/ui_showcase/lib/src/widget/showcase/active_node_notifier.dart similarity index 100% rename from core/ui_showcase/lib/src/widget/active_node_notifier.dart rename to core/ui_showcase/lib/src/widget/showcase/active_node_notifier.dart diff --git a/core/ui_showcase/lib/src/widget/showcase/component_preview.dart b/core/ui_showcase/lib/src/widget/showcase/component_preview.dart new file mode 100644 index 00000000..fc530cd1 --- /dev/null +++ b/core/ui_showcase/lib/src/widget/showcase/component_preview.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; + +class ComponentPreview extends StatelessWidget { + ComponentPreview({ + required this.builder, + List listenables = const [], + this.inputs = const [], + super.key, + }) : listenable = Listenable.merge(listenables); + + final List inputs; + final Listenable listenable; + final WidgetBuilder builder; + + @override + Widget build(BuildContext context) { + final size = MediaQuery.sizeOf(context); + + final child = ListenableBuilder( + listenable: listenable, + builder: (context, child) => Center(child: builder(context)), + ); + + if (size.width < 800) { + return _ComponentPreviewSmall( + inputs: inputs, + child: child, + ); + } + + return _ComponentPreviewStandard( + inputs: inputs, + child: child, + ); + } +} + +class _ComponentPreviewSmall extends StatelessWidget { + const _ComponentPreviewSmall({ + required this.child, + required this.inputs, + }); + + final List inputs; + final Widget child; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + child, + if (inputs.isNotEmpty) + Positioned( + right: 16, + bottom: MediaQuery.paddingOf(context).bottom + 16, + child: FloatingActionButton( + onPressed: () {}, + child: const Icon(Icons.edit_rounded), + ), + ), + ], + ); + } +} + +class _ComponentPreviewStandard extends StatelessWidget { + const _ComponentPreviewStandard({ + required this.child, + required this.inputs, + }); + + final Widget child; + final List inputs; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Row( + children: [ + Expanded(child: child), + if (inputs.isNotEmpty) + DecoratedBox( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainer, + border: Border( + left: BorderSide(color: theme.colorScheme.outlineVariant), + ), + ), + child: SizedBox( + width: 200, + child: ListView( + padding: const EdgeInsets.all(8), + children: inputs, + ), + ), + ), + ], + ); + } +} diff --git a/core/ui_showcase/lib/src/widget/showcase_tree_view.dart b/core/ui_showcase/lib/src/widget/showcase/showcase_tree_view.dart similarity index 100% rename from core/ui_showcase/lib/src/widget/showcase_tree_view.dart rename to core/ui_showcase/lib/src/widget/showcase/showcase_tree_view.dart diff --git a/core/ui_showcase/lib/src/widget/showcase_view.dart b/core/ui_showcase/lib/src/widget/showcase/showcase_view.dart similarity index 69% rename from core/ui_showcase/lib/src/widget/showcase_view.dart rename to core/ui_showcase/lib/src/widget/showcase/showcase_view.dart index e81bdb5c..4df04fb2 100644 --- a/core/ui_showcase/lib/src/widget/showcase_view.dart +++ b/core/ui_showcase/lib/src/widget/showcase/showcase_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:ui_showcase/src/widget/showcase/active_node_metadata.dart'; import 'package:ui_showcase/ui_showcase.dart'; class ShowcaseView extends StatelessWidget { @@ -109,7 +110,7 @@ class ShowcaseMobile extends StatelessWidget { Scaffold.of(context).openDrawer(); }, ), - if (activeNode case final node?) _ActiveNodeMetadata(node: node), + if (activeNode case final activeNode?) ActiveNodeMetadata(node: activeNode), ], ), ), @@ -136,8 +137,6 @@ class ShowcaseDesktop extends StatelessWidget { @override Widget build(BuildContext context) { - final activeNode = ActiveNodeProvider.of(context).activeNode; - return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, body: Row( @@ -150,63 +149,9 @@ class ShowcaseDesktop extends StatelessWidget { selectedNode: ActiveNodeProvider.of(context).activeNode, ), ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (activeNode != null) _ActiveNodeMetadata(node: activeNode), - Expanded(child: navigator), - ], - ), - ), + Expanded(child: navigator), ], ), ); } } - -class _ActiveNodeMetadata extends StatelessWidget { - const _ActiveNodeMetadata({required this.node}); - - final ShowcaseNode node; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _Breadcrumbs(node: node), - if (node.description case final description?) Text(description), - ], - ), - ); - } -} - -class _Breadcrumbs extends StatelessWidget { - const _Breadcrumbs({ - required this.node, - }); - - final ShowcaseNode node; - - @override - Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - final colorScheme = Theme.of(context).colorScheme; - final textStyle = textTheme.bodyMedium?.copyWith(color: colorScheme.secondary); - - return Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - for (final parent in node.getBreadcrumbs()) ...[ - Text(parent.path, style: textStyle), - if (parent != node) Text('/', style: textStyle), - ], - ], - ); - } -} diff --git a/core/ui_showcase/lib/ui_showcase.dart b/core/ui_showcase/lib/ui_showcase.dart index f24aee58..570c4ad9 100644 --- a/core/ui_showcase/lib/ui_showcase.dart +++ b/core/ui_showcase/lib/ui_showcase.dart @@ -1,6 +1,9 @@ export 'src/core/showcase_node.dart'; export 'src/core/showcase_nodes.dart'; export 'src/routing/go_router.dart'; -export 'src/widget/active_node_notifier.dart'; -export 'src/widget/showcase_tree_view.dart'; -export 'src/widget/showcase_view.dart'; +export 'src/widget/inputs/integer_input.dart'; +export 'src/widget/inputs/string_input.dart'; +export 'src/widget/showcase/active_node_notifier.dart'; +export 'src/widget/showcase/component_preview.dart'; +export 'src/widget/showcase/showcase_tree_view.dart'; +export 'src/widget/showcase/showcase_view.dart'; From 55a212f9db28033b62c8dae8ff715fee424e8cc8 Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Sun, 3 Aug 2025 12:46:43 +0200 Subject: [PATCH 11/22] Update showcase node preview --- core/ui_library/example/lib/main.dart | 17 ++-- .../lib/src/previews/button_preview.dart | 19 +++-- .../lib/src/previews/card_preview.dart | 35 -------- .../example/lib/src/widget/sidebar.dart | 28 ++++++ .../lib/src/widget/theme_switcher.dart | 85 +++++++++++++++++++ ...review.dart => showcase_node_preview.dart} | 40 +++++---- core/ui_showcase/lib/ui_showcase.dart | 2 +- 7 files changed, 154 insertions(+), 72 deletions(-) delete mode 100644 core/ui_library/example/lib/src/previews/card_preview.dart create mode 100644 core/ui_library/example/lib/src/widget/sidebar.dart create mode 100644 core/ui_library/example/lib/src/widget/theme_switcher.dart rename core/ui_showcase/lib/src/widget/showcase/{component_preview.dart => showcase_node_preview.dart} (73%) diff --git a/core/ui_library/example/lib/main.dart b/core/ui_library/example/lib/main.dart index 837c61af..52b2c4d2 100644 --- a/core/ui_library/example/lib/main.dart +++ b/core/ui_library/example/lib/main.dart @@ -1,5 +1,5 @@ import 'package:example/src/previews/button_preview.dart'; -import 'package:example/src/previews/card_preview.dart'; +import 'package:example/src/widget/theme_switcher.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:ui_showcase/ui_showcase.dart'; @@ -10,7 +10,6 @@ void main() { final showcaseNodes = [ ShowcaseNode(name: 'Button', widget: const SomeButtonPreview()), - ShowcaseNode(name: 'Card', widget: const SomeCardPreview()), ]; class MyApp extends StatefulWidget { @@ -35,14 +34,12 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { - final darkTheme = ThemeData.dark(); - final lightTheme = ThemeData.light(); - - return MaterialApp.router( - routerConfig: router, - theme: lightTheme, - darkTheme: darkTheme, - debugShowCheckedModeBanner: false, + return ThemeOptionInherited( + builder: (context, theme) => MaterialApp.router( + routerConfig: router, + theme: theme, + debugShowCheckedModeBanner: false, + ), ); } } diff --git a/core/ui_library/example/lib/src/previews/button_preview.dart b/core/ui_library/example/lib/src/previews/button_preview.dart index 2844fc11..e3221444 100644 --- a/core/ui_library/example/lib/src/previews/button_preview.dart +++ b/core/ui_library/example/lib/src/previews/button_preview.dart @@ -1,3 +1,4 @@ +import 'package:example/src/widget/sidebar.dart'; import 'package:flutter/material.dart'; import 'package:ui_showcase/ui_showcase.dart'; @@ -13,15 +14,17 @@ class _SomeButtonPreviewState extends State { @override Widget build(BuildContext context) { - return ComponentPreview( + return ShowcaseNodePreview( listenables: [_controller], - inputs: [ - StringInput( - label: 'Label', - description: 'The label of the button', - controller: _controller, - ), - ], + sidebar: Sidebar( + children: [ + StringInput( + label: 'Label', + description: 'The label of the button', + controller: _controller, + ), + ], + ), builder: (context) { return ElevatedButton( onPressed: () {}, diff --git a/core/ui_library/example/lib/src/previews/card_preview.dart b/core/ui_library/example/lib/src/previews/card_preview.dart deleted file mode 100644 index f1fe5e68..00000000 --- a/core/ui_library/example/lib/src/previews/card_preview.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:ui_showcase/ui_showcase.dart'; - -class SomeCardPreview extends StatefulWidget { - const SomeCardPreview({super.key}); - - @override - State createState() => _SomeCardPreviewState(); -} - -class _SomeCardPreviewState extends State { - final _controller = TextEditingController(text: 'Card'); - - @override - Widget build(BuildContext context) { - return ComponentPreview( - listenables: [_controller], - inputs: [ - StringInput( - label: 'Label', - description: 'The label of the button', - controller: _controller, - ), - ], - builder: (context) { - return Card( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text(_controller.text), - ), - ); - }, - ); - } -} diff --git a/core/ui_library/example/lib/src/widget/sidebar.dart b/core/ui_library/example/lib/src/widget/sidebar.dart new file mode 100644 index 00000000..95090816 --- /dev/null +++ b/core/ui_library/example/lib/src/widget/sidebar.dart @@ -0,0 +1,28 @@ +import 'package:example/src/widget/theme_switcher.dart'; +import 'package:flutter/material.dart'; + +class Sidebar extends StatelessWidget { + const Sidebar({ + required this.children, + this.includeThemeSwitcher = true, + super.key, + }); + + final bool includeThemeSwitcher; + final List children; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + if (includeThemeSwitcher) const ThemeOptionInput(), + ...children, + ], + ), + ); + } +} diff --git a/core/ui_library/example/lib/src/widget/theme_switcher.dart b/core/ui_library/example/lib/src/widget/theme_switcher.dart new file mode 100644 index 00000000..300c6958 --- /dev/null +++ b/core/ui_library/example/lib/src/widget/theme_switcher.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; + +final lightTheme = ThemeData.light(); +final darkTheme = ThemeData.dark(); + +class ThemeOptionInput extends StatelessWidget { + const ThemeOptionInput({super.key}); + + static final _entries = >[ + DropdownMenuEntry(value: lightTheme, label: 'Light'), + DropdownMenuEntry(value: darkTheme, label: 'Dark'), + ]; + + @override + Widget build(BuildContext context) { + final theme = ThemeOptionInherited.of(context); + + return DropdownMenu( + dropdownMenuEntries: _entries, + initialSelection: theme ?? _entries.first.value, + onSelected: (value) { + if (value == null) return; + ThemeOptionInherited.setTheme(context, value); + }, + ); + } +} + +class ThemeOptionInherited extends StatefulWidget { + const ThemeOptionInherited({this.child, this.builder, super.key}); + + final Widget? child; + final Widget Function(BuildContext context, ThemeData theme)? builder; + + static void setTheme(BuildContext context, ThemeData theme) { + final inherited = + context.getElementForInheritedWidgetOfExactType<_ThemeOptionInherited>()?.widget + as _ThemeOptionInherited?; + inherited?.state.setTheme(theme); + } + + static ThemeData? of(BuildContext context, {bool listen = true}) { + final inherited = listen + ? context.dependOnInheritedWidgetOfExactType<_ThemeOptionInherited>() + : context.getElementForInheritedWidgetOfExactType<_ThemeOptionInherited>()?.widget + as _ThemeOptionInherited?; + return inherited?.theme; + } + + @override + State createState() => ThemeOptionInheritedState(); +} + +class ThemeOptionInheritedState extends State { + ThemeData _theme = lightTheme; + + void setTheme(ThemeData theme) { + if (_theme != theme) { + setState(() { + _theme = theme; + }); + } + } + + @override + Widget build(BuildContext context) { + return _ThemeOptionInherited( + theme: _theme, + state: this, + child: widget.builder?.call(context, _theme) ?? widget.child!, + ); + } +} + +class _ThemeOptionInherited extends InheritedWidget { + const _ThemeOptionInherited({required this.theme, required super.child, required this.state}); + + final ThemeOptionInheritedState state; + final ThemeData theme; + + @override + bool updateShouldNotify(_ThemeOptionInherited oldWidget) { + return theme != oldWidget.theme; + } +} diff --git a/core/ui_showcase/lib/src/widget/showcase/component_preview.dart b/core/ui_showcase/lib/src/widget/showcase/showcase_node_preview.dart similarity index 73% rename from core/ui_showcase/lib/src/widget/showcase/component_preview.dart rename to core/ui_showcase/lib/src/widget/showcase/showcase_node_preview.dart index fc530cd1..a1ef0777 100644 --- a/core/ui_showcase/lib/src/widget/showcase/component_preview.dart +++ b/core/ui_showcase/lib/src/widget/showcase/showcase_node_preview.dart @@ -1,35 +1,41 @@ import 'package:flutter/material.dart'; -class ComponentPreview extends StatelessWidget { - ComponentPreview({ +class ShowcaseNodePreview extends StatelessWidget { + ShowcaseNodePreview({ required this.builder, List listenables = const [], - this.inputs = const [], + this.sidebar, + this.wrapWith, super.key, }) : listenable = Listenable.merge(listenables); - final List inputs; + final Widget? sidebar; final Listenable listenable; final WidgetBuilder builder; + final Widget Function(Widget child)? wrapWith; @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); - final child = ListenableBuilder( + Widget child = ListenableBuilder( listenable: listenable, - builder: (context, child) => Center(child: builder(context)), + builder: (context, child) => builder(context), ); + if (wrapWith != null) { + child = wrapWith!(child); + } + if (size.width < 800) { return _ComponentPreviewSmall( - inputs: inputs, + sidebar: sidebar, child: child, ); } return _ComponentPreviewStandard( - inputs: inputs, + sidebar: sidebar, child: child, ); } @@ -38,10 +44,10 @@ class ComponentPreview extends StatelessWidget { class _ComponentPreviewSmall extends StatelessWidget { const _ComponentPreviewSmall({ required this.child, - required this.inputs, + required this.sidebar, }); - final List inputs; + final Widget? sidebar; final Widget child; @override @@ -49,7 +55,7 @@ class _ComponentPreviewSmall extends StatelessWidget { return Stack( children: [ child, - if (inputs.isNotEmpty) + if (sidebar != null) Positioned( right: 16, bottom: MediaQuery.paddingOf(context).bottom + 16, @@ -66,11 +72,11 @@ class _ComponentPreviewSmall extends StatelessWidget { class _ComponentPreviewStandard extends StatelessWidget { const _ComponentPreviewStandard({ required this.child, - required this.inputs, + required this.sidebar, }); final Widget child; - final List inputs; + final Widget? sidebar; @override Widget build(BuildContext context) { @@ -79,7 +85,7 @@ class _ComponentPreviewStandard extends StatelessWidget { return Row( children: [ Expanded(child: child), - if (inputs.isNotEmpty) + if (sidebar != null) DecoratedBox( decoration: BoxDecoration( color: theme.colorScheme.surfaceContainer, @@ -89,10 +95,8 @@ class _ComponentPreviewStandard extends StatelessWidget { ), child: SizedBox( width: 200, - child: ListView( - padding: const EdgeInsets.all(8), - children: inputs, - ), + height: double.infinity, + child: sidebar, ), ), ], diff --git a/core/ui_showcase/lib/ui_showcase.dart b/core/ui_showcase/lib/ui_showcase.dart index 570c4ad9..84e242ee 100644 --- a/core/ui_showcase/lib/ui_showcase.dart +++ b/core/ui_showcase/lib/ui_showcase.dart @@ -4,6 +4,6 @@ export 'src/routing/go_router.dart'; export 'src/widget/inputs/integer_input.dart'; export 'src/widget/inputs/string_input.dart'; export 'src/widget/showcase/active_node_notifier.dart'; -export 'src/widget/showcase/component_preview.dart'; +export 'src/widget/showcase/showcase_node_preview.dart'; export 'src/widget/showcase/showcase_tree_view.dart'; export 'src/widget/showcase/showcase_view.dart'; From bf3b845400d8d938bebc51eb0a366e323dd03229 Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Sun, 3 Aug 2025 12:59:35 +0200 Subject: [PATCH 12/22] Add device preview --- core/ui_library/example/lib/main.dart | 14 +- .../lib/src/previews/button_preview.dart | 21 +- .../lib/src/widget/component_preview.dart | 41 ++++ core/ui_library/example/pubspec.lock | 191 +++++++++++++++++- core/ui_library/example/pubspec.yaml | 1 + .../lib/src/widget/inputs/input_widget.dart | 5 + .../lib/src/widget/inputs/integer_input.dart | 6 +- .../lib/src/widget/inputs/string_input.dart | 6 +- .../showcase/showcase_node_preview.dart | 10 +- core/ui_showcase/lib/ui_showcase.dart | 1 + 10 files changed, 263 insertions(+), 33 deletions(-) create mode 100644 core/ui_library/example/lib/src/widget/component_preview.dart create mode 100644 core/ui_showcase/lib/src/widget/inputs/input_widget.dart diff --git a/core/ui_library/example/lib/main.dart b/core/ui_library/example/lib/main.dart index 52b2c4d2..0cfa83ba 100644 --- a/core/ui_library/example/lib/main.dart +++ b/core/ui_library/example/lib/main.dart @@ -1,5 +1,4 @@ import 'package:example/src/previews/button_preview.dart'; -import 'package:example/src/widget/theme_switcher.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:ui_showcase/ui_showcase.dart'; @@ -33,13 +32,8 @@ class _MyAppState extends State { } @override - Widget build(BuildContext context) { - return ThemeOptionInherited( - builder: (context, theme) => MaterialApp.router( - routerConfig: router, - theme: theme, - debugShowCheckedModeBanner: false, - ), - ); - } + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: router, + debugShowCheckedModeBanner: false, + ); } diff --git a/core/ui_library/example/lib/src/previews/button_preview.dart b/core/ui_library/example/lib/src/previews/button_preview.dart index e3221444..a210f509 100644 --- a/core/ui_library/example/lib/src/previews/button_preview.dart +++ b/core/ui_library/example/lib/src/previews/button_preview.dart @@ -1,4 +1,4 @@ -import 'package:example/src/widget/sidebar.dart'; +import 'package:example/src/widget/component_preview.dart'; import 'package:flutter/material.dart'; import 'package:ui_showcase/ui_showcase.dart'; @@ -14,17 +14,14 @@ class _SomeButtonPreviewState extends State { @override Widget build(BuildContext context) { - return ShowcaseNodePreview( - listenables: [_controller], - sidebar: Sidebar( - children: [ - StringInput( - label: 'Label', - description: 'The label of the button', - controller: _controller, - ), - ], - ), + return ComponentPreview( + inputs: [ + StringInput( + label: 'Label', + description: 'The label of the button', + controller: _controller, + ), + ], builder: (context) { return ElevatedButton( onPressed: () {}, diff --git a/core/ui_library/example/lib/src/widget/component_preview.dart b/core/ui_library/example/lib/src/widget/component_preview.dart new file mode 100644 index 00000000..377367b4 --- /dev/null +++ b/core/ui_library/example/lib/src/widget/component_preview.dart @@ -0,0 +1,41 @@ +import 'package:device_preview/device_preview.dart'; +import 'package:example/src/widget/sidebar.dart'; +import 'package:example/src/widget/theme_switcher.dart'; +import 'package:flutter/material.dart'; +import 'package:ui_showcase/ui_showcase.dart'; + +class ComponentPreview extends StatelessWidget { + const ComponentPreview({ + required this.inputs, + required this.builder, + super.key, + }); + + final List inputs; + final WidgetBuilder builder; + + @override + Widget build(BuildContext context) => ThemeOptionInherited( + builder: (context, theme) { + return ShowcaseNodePreview( + builder: builder, + listenables: inputs.map((e) => e.listenable).toList(growable: false), + sidebar: Sidebar(children: inputs), + wrapWith: (child) { + return DevicePreview( + isToolbarVisible: false, + builder: (context) { + return MaterialApp( + theme: theme, + // ignore: deprecated_member_use + useInheritedMediaQuery: true, + home: Scaffold(body: Center(child: child)), + builder: DevicePreview.appBuilder, + ); + }, + ); + }, + ); + }, + ); +} diff --git a/core/ui_library/example/pubspec.lock b/core/ui_library/example/pubspec.lock index ae9810ee..6e12cca3 100644 --- a/core/ui_library/example/pubspec.lock +++ b/core/ui_library/example/pubspec.lock @@ -41,6 +41,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + device_frame: + dependency: transitive + description: + name: device_frame + sha256: "7b2ebb2a09d6cc0f086b51bd1412d7be83e0170056a7290349169be41164c86a" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + device_preview: + dependency: "direct main" + description: + name: device_preview + sha256: "88aa1cc73ee9a8ec771b309dcbc4000cc66b5d8456b825980997640ab1195bf5" + url: "https://pub.dev" + source: hosted + version: "1.3.1" fake_async: dependency: transitive description: @@ -49,11 +65,32 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -64,6 +101,14 @@ packages: description: flutter source: sdk version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.dev" + source: hosted + version: "3.1.0" go_router: dependency: "direct main" description: @@ -72,6 +117,22 @@ packages: url: "https://pub.dev" source: hosted version: "16.0.0" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" leak_tracker: dependency: transitive description: @@ -128,6 +189,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -136,6 +205,110 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + provider: + dependency: transitive + description: + name: provider + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -220,6 +393,22 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: dart: ">=3.8.0 <4.0.0" - flutter: ">=3.27.0" + flutter: ">=3.32.0" diff --git a/core/ui_library/example/pubspec.yaml b/core/ui_library/example/pubspec.yaml index 0d1132df..5cfbcbb1 100644 --- a/core/ui_library/example/pubspec.yaml +++ b/core/ui_library/example/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: ui_showcase: path: ../../ui_showcase go_router: ^16.0.0 + device_preview: ^1.3.1 dev_dependencies: flutter_test: diff --git a/core/ui_showcase/lib/src/widget/inputs/input_widget.dart b/core/ui_showcase/lib/src/widget/inputs/input_widget.dart new file mode 100644 index 00000000..3dfcd85f --- /dev/null +++ b/core/ui_showcase/lib/src/widget/inputs/input_widget.dart @@ -0,0 +1,5 @@ +import 'package:flutter/widgets.dart'; + +mixin InputWidget on Widget { + Listenable get listenable; +} diff --git a/core/ui_showcase/lib/src/widget/inputs/integer_input.dart b/core/ui_showcase/lib/src/widget/inputs/integer_input.dart index 00b63c48..83722e2f 100644 --- a/core/ui_showcase/lib/src/widget/inputs/integer_input.dart +++ b/core/ui_showcase/lib/src/widget/inputs/integer_input.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:ui_showcase/ui_showcase.dart'; -class IntegerInput extends StatelessWidget { +class IntegerInput extends StatelessWidget with InputWidget { const IntegerInput({ required this.notifier, required this.label, @@ -15,6 +16,9 @@ class IntegerInput extends StatelessWidget { final int? max; final String? description; + @override + Listenable get listenable => notifier; + @override Widget build(BuildContext context) { final theme = Theme.of(context); diff --git a/core/ui_showcase/lib/src/widget/inputs/string_input.dart b/core/ui_showcase/lib/src/widget/inputs/string_input.dart index 61b059b9..ee8ff721 100644 --- a/core/ui_showcase/lib/src/widget/inputs/string_input.dart +++ b/core/ui_showcase/lib/src/widget/inputs/string_input.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:ui_showcase/ui_showcase.dart'; -class StringInput extends StatelessWidget { +class StringInput extends StatelessWidget with InputWidget { const StringInput({ required this.label, required this.controller, @@ -13,6 +14,9 @@ class StringInput extends StatelessWidget { final String? description; final String? hint; + @override + Listenable get listenable => controller; + @override Widget build(BuildContext context) { final theme = Theme.of(context); diff --git a/core/ui_showcase/lib/src/widget/showcase/showcase_node_preview.dart b/core/ui_showcase/lib/src/widget/showcase/showcase_node_preview.dart index a1ef0777..87c9483b 100644 --- a/core/ui_showcase/lib/src/widget/showcase/showcase_node_preview.dart +++ b/core/ui_showcase/lib/src/widget/showcase/showcase_node_preview.dart @@ -28,16 +28,10 @@ class ShowcaseNodePreview extends StatelessWidget { } if (size.width < 800) { - return _ComponentPreviewSmall( - sidebar: sidebar, - child: child, - ); + return _ComponentPreviewSmall(sidebar: sidebar, child: child); } - return _ComponentPreviewStandard( - sidebar: sidebar, - child: child, - ); + return _ComponentPreviewStandard(sidebar: sidebar, child: child); } } diff --git a/core/ui_showcase/lib/ui_showcase.dart b/core/ui_showcase/lib/ui_showcase.dart index 84e242ee..dedf8b6d 100644 --- a/core/ui_showcase/lib/ui_showcase.dart +++ b/core/ui_showcase/lib/ui_showcase.dart @@ -1,6 +1,7 @@ export 'src/core/showcase_node.dart'; export 'src/core/showcase_nodes.dart'; export 'src/routing/go_router.dart'; +export 'src/widget/inputs/input_widget.dart'; export 'src/widget/inputs/integer_input.dart'; export 'src/widget/inputs/string_input.dart'; export 'src/widget/showcase/active_node_notifier.dart'; From 4b202f8a783377b298b5221a493049dec758d70c Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Sun, 3 Aug 2025 13:06:40 +0200 Subject: [PATCH 13/22] Add Card preview to showcase - Introduced `CardPreview` component to showcase card functionality. - Updated main application to include the new `CardPreview` in the showcase nodes. - Added `ShowcaseNodeMixin` to enhance integration with showcase nodes. --- core/ui_library/example/lib/main.dart | 2 ++ .../lib/src/previews/card_preview.dart | 35 +++++++++++++++++++ .../lib/src/core/showcase_node.dart | 4 +++ 3 files changed, 41 insertions(+) create mode 100644 core/ui_library/example/lib/src/previews/card_preview.dart diff --git a/core/ui_library/example/lib/main.dart b/core/ui_library/example/lib/main.dart index 0cfa83ba..f92b1d8e 100644 --- a/core/ui_library/example/lib/main.dart +++ b/core/ui_library/example/lib/main.dart @@ -1,4 +1,5 @@ import 'package:example/src/previews/button_preview.dart'; +import 'package:example/src/previews/card_preview.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:ui_showcase/ui_showcase.dart'; @@ -9,6 +10,7 @@ void main() { final showcaseNodes = [ ShowcaseNode(name: 'Button', widget: const SomeButtonPreview()), + ShowcaseNode(name: 'Card', widget: const CardPreview()), ]; class MyApp extends StatefulWidget { diff --git a/core/ui_library/example/lib/src/previews/card_preview.dart b/core/ui_library/example/lib/src/previews/card_preview.dart new file mode 100644 index 00000000..714be458 --- /dev/null +++ b/core/ui_library/example/lib/src/previews/card_preview.dart @@ -0,0 +1,35 @@ +import 'package:example/src/widget/component_preview.dart'; +import 'package:flutter/material.dart'; +import 'package:ui_showcase/ui_showcase.dart'; + +class CardPreview extends StatefulWidget with ShowcaseNodeMixin { + const CardPreview({super.key}); + + @override + State createState() => _CardPreviewState(); +} + +class _CardPreviewState extends State { + final _controller = TextEditingController(text: 'Card'); + + @override + Widget build(BuildContext context) { + return ComponentPreview( + inputs: [ + StringInput( + label: 'Label', + description: 'The label of the button', + controller: _controller, + ), + ], + builder: (context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(_controller.text), + ), + ); + }, + ); + } +} diff --git a/core/ui_showcase/lib/src/core/showcase_node.dart b/core/ui_showcase/lib/src/core/showcase_node.dart index 3bf31eab..939f2580 100644 --- a/core/ui_showcase/lib/src/core/showcase_node.dart +++ b/core/ui_showcase/lib/src/core/showcase_node.dart @@ -111,3 +111,7 @@ class ShowcaseNode { return breadcrumbs; } } + +mixin ShowcaseNodeMixin on Widget implements ShowcaseNode { + +} From f30d8463d57e46bbca72289028e4deed9c099c97 Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Sun, 3 Aug 2025 13:14:17 +0200 Subject: [PATCH 14/22] Refactor CardPreview and remove ShowcaseNodeMixin - Simplified `CardPreview` by removing the `ShowcaseNodeMixin`. - Cleaned up `ShowcaseNode` by deleting the unused mixin code, enhancing maintainability. --- core/ui_library/example/lib/src/previews/card_preview.dart | 2 +- core/ui_showcase/lib/src/core/showcase_node.dart | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/core/ui_library/example/lib/src/previews/card_preview.dart b/core/ui_library/example/lib/src/previews/card_preview.dart index 714be458..5f412c63 100644 --- a/core/ui_library/example/lib/src/previews/card_preview.dart +++ b/core/ui_library/example/lib/src/previews/card_preview.dart @@ -2,7 +2,7 @@ import 'package:example/src/widget/component_preview.dart'; import 'package:flutter/material.dart'; import 'package:ui_showcase/ui_showcase.dart'; -class CardPreview extends StatefulWidget with ShowcaseNodeMixin { +class CardPreview extends StatefulWidget { const CardPreview({super.key}); @override diff --git a/core/ui_showcase/lib/src/core/showcase_node.dart b/core/ui_showcase/lib/src/core/showcase_node.dart index 939f2580..3bf31eab 100644 --- a/core/ui_showcase/lib/src/core/showcase_node.dart +++ b/core/ui_showcase/lib/src/core/showcase_node.dart @@ -111,7 +111,3 @@ class ShowcaseNode { return breadcrumbs; } } - -mixin ShowcaseNodeMixin on Widget implements ShowcaseNode { - -} From 20c28bf10eea388fd692c3a2430be22175f5a247 Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Sun, 3 Aug 2025 14:46:22 +0200 Subject: [PATCH 15/22] Enhance button preview and add boolean input widget - Refactored `SomeButtonPreview` to use `ValueNotifier` for enabling/disabling the button and updated the label controller. - Introduced a new `BooleanInput` widget for handling boolean values with a switch interface. - Adjusted spacing in the `Sidebar` widget for improved layout. - Updated exports in `ui_showcase.dart` to include the new `BooleanInput` component. --- .../lib/src/previews/button_preview.dart | 14 ++++-- .../example/lib/src/widget/sidebar.dart | 2 +- .../lib/src/widget/inputs/boolean_input.dart | 47 +++++++++++++++++++ core/ui_showcase/lib/ui_showcase.dart | 1 + 4 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 core/ui_showcase/lib/src/widget/inputs/boolean_input.dart diff --git a/core/ui_library/example/lib/src/previews/button_preview.dart b/core/ui_library/example/lib/src/previews/button_preview.dart index a210f509..65ed50c3 100644 --- a/core/ui_library/example/lib/src/previews/button_preview.dart +++ b/core/ui_library/example/lib/src/previews/button_preview.dart @@ -10,7 +10,8 @@ class SomeButtonPreview extends StatefulWidget { } class _SomeButtonPreviewState extends State { - final _controller = TextEditingController(text: 'Click Me!'); + final _labelController = TextEditingController(text: 'Click Me!'); + final _enabledController = ValueNotifier(true); @override Widget build(BuildContext context) { @@ -19,13 +20,18 @@ class _SomeButtonPreviewState extends State { StringInput( label: 'Label', description: 'The label of the button', - controller: _controller, + controller: _labelController, + ), + BooleanInput( + label: 'Enabled', + description: 'Whether the button is enabled', + notifier: _enabledController, ), ], builder: (context) { return ElevatedButton( - onPressed: () {}, - child: Text(_controller.text), + onPressed: _enabledController.value ? () {} : null, + child: Text(_labelController.text), ); }, ); diff --git a/core/ui_library/example/lib/src/widget/sidebar.dart b/core/ui_library/example/lib/src/widget/sidebar.dart index 95090816..05f14ad6 100644 --- a/core/ui_library/example/lib/src/widget/sidebar.dart +++ b/core/ui_library/example/lib/src/widget/sidebar.dart @@ -17,7 +17,7 @@ class Sidebar extends StatelessWidget { padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - spacing: 8, + spacing: 24, children: [ if (includeThemeSwitcher) const ThemeOptionInput(), ...children, diff --git a/core/ui_showcase/lib/src/widget/inputs/boolean_input.dart b/core/ui_showcase/lib/src/widget/inputs/boolean_input.dart new file mode 100644 index 00000000..e5d4d8d3 --- /dev/null +++ b/core/ui_showcase/lib/src/widget/inputs/boolean_input.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:ui_showcase/ui_showcase.dart'; + +class BooleanInput extends StatelessWidget with InputWidget { + const BooleanInput({ + required this.label, + required this.notifier, + this.description, + super.key, + }); + + final String label; + final String? description; + final ValueNotifier notifier; + + @override + Listenable get listenable => notifier; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final colorScheme = theme.colorScheme; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: textTheme.labelLarge), + ValueListenableBuilder( + valueListenable: notifier, + builder: (context, value, child) { + return Switch( + value: value, + onChanged: (value) => notifier.value = value, + ); + }, + ), + if (description case final description?) + Text( + description, + style: textTheme.bodySmall?.copyWith(color: colorScheme.secondary), + ), + ], + ); + } +} diff --git a/core/ui_showcase/lib/ui_showcase.dart b/core/ui_showcase/lib/ui_showcase.dart index dedf8b6d..3c885290 100644 --- a/core/ui_showcase/lib/ui_showcase.dart +++ b/core/ui_showcase/lib/ui_showcase.dart @@ -1,6 +1,7 @@ export 'src/core/showcase_node.dart'; export 'src/core/showcase_nodes.dart'; export 'src/routing/go_router.dart'; +export 'src/widget/inputs/boolean_input.dart'; export 'src/widget/inputs/input_widget.dart'; export 'src/widget/inputs/integer_input.dart'; export 'src/widget/inputs/string_input.dart'; From e59a0e679e6100830d47dd01a77bce82b8a8f01d Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Mon, 4 Aug 2025 10:49:04 +0200 Subject: [PATCH 16/22] Add checkbox preview --- core/ui_library/example/devtools_options.yaml | 3 ++ core/ui_library/example/lib/main.dart | 2 ++ .../lib/src/previews/checkbox_preview.dart | 35 +++++++++++++++++++ devtools_options.yaml | 3 ++ 4 files changed, 43 insertions(+) create mode 100644 core/ui_library/example/devtools_options.yaml create mode 100644 core/ui_library/example/lib/src/previews/checkbox_preview.dart create mode 100644 devtools_options.yaml diff --git a/core/ui_library/example/devtools_options.yaml b/core/ui_library/example/devtools_options.yaml new file mode 100644 index 00000000..fa0b357c --- /dev/null +++ b/core/ui_library/example/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/core/ui_library/example/lib/main.dart b/core/ui_library/example/lib/main.dart index f92b1d8e..172d737b 100644 --- a/core/ui_library/example/lib/main.dart +++ b/core/ui_library/example/lib/main.dart @@ -1,5 +1,6 @@ import 'package:example/src/previews/button_preview.dart'; import 'package:example/src/previews/card_preview.dart'; +import 'package:example/src/previews/checkbox_preview.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:ui_showcase/ui_showcase.dart'; @@ -11,6 +12,7 @@ void main() { final showcaseNodes = [ ShowcaseNode(name: 'Button', widget: const SomeButtonPreview()), ShowcaseNode(name: 'Card', widget: const CardPreview()), + ShowcaseNode(name: 'Checkbox', widget: const CheckboxPreview()), ]; class MyApp extends StatefulWidget { diff --git a/core/ui_library/example/lib/src/previews/checkbox_preview.dart b/core/ui_library/example/lib/src/previews/checkbox_preview.dart new file mode 100644 index 00000000..e9baa57d --- /dev/null +++ b/core/ui_library/example/lib/src/previews/checkbox_preview.dart @@ -0,0 +1,35 @@ +import 'package:example/src/widget/component_preview.dart'; +import 'package:flutter/material.dart'; +import 'package:ui_showcase/ui_showcase.dart'; + +class CheckboxPreview extends StatefulWidget { + const CheckboxPreview({super.key}); + + @override + State createState() => _CheckboxPreviewState(); +} + +class _CheckboxPreviewState extends State { + final _enabledController = ValueNotifier(true); + + @override + Widget build(BuildContext context) { + return ComponentPreview( + inputs: [ + BooleanInput( + label: 'Value', + description: 'The value of the checkbox', + notifier: _enabledController, + ), + ], + builder: (context) { + return Checkbox( + value: _enabledController.value, + onChanged: (value) { + _enabledController.value = value ?? false; + }, + ); + }, + ); + } +} diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 00000000..fa0b357c --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: From 2e87638cec1a1feb6fd1ec74b5bd09af92f3a583 Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Fri, 8 Aug 2025 18:16:48 +0200 Subject: [PATCH 17/22] Decouple the routing solution --- .../lib/src/widget/component_preview.dart | 15 +-- .../lib/src/routing/go_router.dart | 59 ++++++++++- .../widget/showcase/active_node_notifier.dart | 97 ++++--------------- .../src/widget/showcase/showcase_view.dart | 32 ++---- 4 files changed, 86 insertions(+), 117 deletions(-) diff --git a/core/ui_library/example/lib/src/widget/component_preview.dart b/core/ui_library/example/lib/src/widget/component_preview.dart index 377367b4..b171428f 100644 --- a/core/ui_library/example/lib/src/widget/component_preview.dart +++ b/core/ui_library/example/lib/src/widget/component_preview.dart @@ -1,4 +1,3 @@ -import 'package:device_preview/device_preview.dart'; import 'package:example/src/widget/sidebar.dart'; import 'package:example/src/widget/theme_switcher.dart'; import 'package:flutter/material.dart'; @@ -22,17 +21,9 @@ class ComponentPreview extends StatelessWidget { listenables: inputs.map((e) => e.listenable).toList(growable: false), sidebar: Sidebar(children: inputs), wrapWith: (child) { - return DevicePreview( - isToolbarVisible: false, - builder: (context) { - return MaterialApp( - theme: theme, - // ignore: deprecated_member_use - useInheritedMediaQuery: true, - home: Scaffold(body: Center(child: child)), - builder: DevicePreview.appBuilder, - ); - }, + return MaterialApp( + theme: theme, + home: Scaffold(body: Center(child: child)), ); }, ); diff --git a/core/ui_showcase/lib/src/routing/go_router.dart b/core/ui_showcase/lib/src/routing/go_router.dart index dff01ded..38b298ef 100644 --- a/core/ui_showcase/lib/src/routing/go_router.dart +++ b/core/ui_showcase/lib/src/routing/go_router.dart @@ -13,9 +13,9 @@ RouteBase createRootShowcaseRoute(List nodes) { routes: _createRoutes(nodes), ), ], - builder: (context, state, child) => ShowcaseView( + builder: (context, state, child) => GoRouterNotifier( nodes: nodes, - navigator: child, + child: ShowcaseView(nodes: nodes, navigator: child), ), ); } @@ -39,3 +39,58 @@ List _createRoutes(List nodes) { return routes; } + +class NodeRouterGoRouter extends ValueNotifier implements NodeRouter { + NodeRouterGoRouter(this._router, this._nodes) : super(null) { + _router.routerDelegate.addListener(_onLocationChanged); + } + + final GoRouter _router; + final List _nodes; + + @override + ShowcaseNode? get activeNode => value; + + void _onLocationChanged() { + final path = _router.state.fullPath ?? ''; + final node = ShowcaseNodes(_nodes).findNodeByPath(path); + + value = node; + } + + @override + void updateActiveNode(ShowcaseNode? node) { + _router.go('/${node?.fullPath ?? ''}'); + } +} + +class GoRouterNotifier extends StatefulWidget { + const GoRouterNotifier({ + required this.nodes, + required this.child, + super.key, + }); + + final List nodes; + final Widget child; + + @override + State createState() => _GoRouterNotifierState(); +} + +class _GoRouterNotifierState extends State { + late final NodeRouterGoRouter _nodeRouter; + late final GoRouter _router; + + @override + void initState() { + super.initState(); + _router = GoRouter.of(context); + _nodeRouter = NodeRouterGoRouter(_router, widget.nodes); + } + + @override + Widget build(BuildContext context) { + return ActiveNodeNotifier(nodeRouter: _nodeRouter, child: widget.child); + } +} diff --git a/core/ui_showcase/lib/src/widget/showcase/active_node_notifier.dart b/core/ui_showcase/lib/src/widget/showcase/active_node_notifier.dart index 51c6e59a..bdf3fbbe 100644 --- a/core/ui_showcase/lib/src/widget/showcase/active_node_notifier.dart +++ b/core/ui_showcase/lib/src/widget/showcase/active_node_notifier.dart @@ -1,92 +1,33 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:ui_showcase/ui_showcase.dart'; -class ActiveNodeProvider extends StatefulWidget { - const ActiveNodeProvider({ - required this.nodes, - required this.child, - super.key, - }); +abstract class NodeRouter implements Listenable { + void updateActiveNode(ShowcaseNode? node); - final List nodes; - final Widget child; - - static ActiveNodeProviderState of(BuildContext context, {bool listen = true}) { - final widget = listen - ? context.dependOnInheritedWidgetOfExactType<_InheritedNodeNotifier>() - : context.getInheritedWidgetOfExactType<_InheritedNodeNotifier>(); - - if (widget == null) { - throw FlutterError('No ActiveNodeProvider found in context'); - } - - return widget.state; - } - - @override - State createState() => ActiveNodeProviderState(); -} - -class ActiveNodeProviderState extends State { - late final ActiveNodeNotifier _notifier; - late final GoRouter _router; - - @override - void initState() { - super.initState(); - _router = GoRouter.of(context); - _notifier = ActiveNodeNotifier(router: _router, nodes: widget.nodes); - } - - ShowcaseNode? get activeNode => _notifier.value; - - void updateActiveNode(ShowcaseNode? node) { - _router.go('/${node?.fullPath ?? ''}'); - } - - @override - Widget build(BuildContext context) { - return _InheritedNodeNotifier( - state: this, - notifier: _notifier, - child: widget.child, - ); - } + ShowcaseNode? get activeNode; } -class ActiveNodeNotifier extends ValueNotifier { - ActiveNodeNotifier({ - required this.router, - required this.nodes, - }) : super(_nodeFromPath(router.state.fullPath ?? '', nodes)) { - router.routerDelegate.addListener(_onRouteChange); - } +class ActiveNodeNotifier extends InheritedNotifier { + const ActiveNodeNotifier({ + required this.nodeRouter, + required super.child, + }) : super(notifier: nodeRouter); - final GoRouter router; - final List nodes; + final NodeRouter nodeRouter; - static ShowcaseNode? _nodeFromPath(String path, List nodes) { - return ShowcaseNodes(nodes).findNodeByPath(path.replaceFirst('/', '')); - } + static ActiveNodeNotifier of(BuildContext context, {bool listen = true}) { + final widget = listen + ? context.dependOnInheritedWidgetOfExactType() + : context.getInheritedWidgetOfExactType(); - void _onRouteChange() { - value = _nodeFromPath(router.state.fullPath ?? '', nodes); - } + if (widget == null) { + throw FlutterError('No ActiveNodeNotifier found in context'); + } - @override - void dispose() { - router.routerDelegate.removeListener(_onRouteChange); - super.dispose(); + return widget; } -} -class _InheritedNodeNotifier extends InheritedNotifier { - const _InheritedNodeNotifier({ - required super.notifier, - required super.child, - required this.state, - }); + ShowcaseNode? get activeNode => nodeRouter.activeNode; - final ActiveNodeProviderState state; + void updateActiveNode(ShowcaseNode? node) => nodeRouter.updateActiveNode(node); } diff --git a/core/ui_showcase/lib/src/widget/showcase/showcase_view.dart b/core/ui_showcase/lib/src/widget/showcase/showcase_view.dart index 4df04fb2..54c53990 100644 --- a/core/ui_showcase/lib/src/widget/showcase/showcase_view.dart +++ b/core/ui_showcase/lib/src/widget/showcase/showcase_view.dart @@ -2,31 +2,13 @@ import 'package:flutter/material.dart'; import 'package:ui_showcase/src/widget/showcase/active_node_metadata.dart'; import 'package:ui_showcase/ui_showcase.dart'; -class ShowcaseView extends StatelessWidget { - const ShowcaseView({ - required this.nodes, - required this.navigator, - }); - - final List nodes; - final Widget navigator; - - @override - Widget build(BuildContext context) { - return ActiveNodeProvider( - nodes: nodes, - child: _ShowcaseView(nodes: nodes, navigator: navigator), - ); - } -} - /// A view that displays a list of showcase nodes. /// /// See more: /// - [ShowcaseMobile] /// - [ShowcaseDesktop] -class _ShowcaseView extends StatefulWidget { - const _ShowcaseView({ +class ShowcaseView extends StatefulWidget { + const ShowcaseView({ required this.nodes, required this.navigator, }); @@ -35,12 +17,12 @@ class _ShowcaseView extends StatefulWidget { final Widget navigator; @override - State<_ShowcaseView> createState() => _ShowcaseViewState(); + State createState() => _ShowcaseViewState(); } -class _ShowcaseViewState extends State<_ShowcaseView> { +class _ShowcaseViewState extends State { void _onNodeSelected(ShowcaseNode node) { - ActiveNodeProvider.of(context, listen: false).updateActiveNode(node); + ActiveNodeNotifier.of(context, listen: false).updateActiveNode(node); } @override @@ -80,7 +62,7 @@ class ShowcaseMobile extends StatelessWidget { @override Widget build(BuildContext context) { - final activeNode = ActiveNodeProvider.of(context).activeNode; + final activeNode = ActiveNodeNotifier.of(context).activeNode; return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, @@ -146,7 +128,7 @@ class ShowcaseDesktop extends StatelessWidget { child: ShowcaseTreeView( nodes: nodes, onNodeSelected: onNodeSelected, - selectedNode: ActiveNodeProvider.of(context).activeNode, + selectedNode: ActiveNodeNotifier.of(context).activeNode, ), ), Expanded(child: navigator), From da275468a0dec001e52fbd476c79abe091a5eef0 Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Sat, 9 Aug 2025 15:04:23 +0200 Subject: [PATCH 18/22] Implement theme selection functionality in showcase - Refactored the routing to support theme options through `ThemeOptionProvider`. - Introduced `ThemeOptionInput` for selecting themes within the UI. - Updated `ComponentPreview` to utilize the new theme management system. - Removed the deprecated `theme_switcher.dart` file and adjusted imports accordingly. - Enhanced input widgets (`BooleanInput`, `IntegerInput`, `StringInput`) to support state restoration using `PageStorageReader` mixin. --- core/ui_library/example/lib/main.dart | 11 +- .../lib/src/widget/component_preview.dart | 22 ++-- .../example/lib/src/widget/sidebar.dart | 2 +- .../lib/src/widget/theme_switcher.dart | 85 -------------- .../lib/src/routing/go_router.dart | 23 +++- .../lib/src/widget/inputs/boolean_input.dart | 36 ++++-- .../lib/src/widget/inputs/integer_input.dart | 38 ++++-- .../lib/src/widget/inputs/string_input.dart | 29 ++++- .../lib/src/widget/inputs/theme_input.dart | 108 ++++++++++++++++++ .../restoration/page_storage_reader.dart | 33 ++++++ core/ui_showcase/lib/ui_showcase.dart | 1 + 11 files changed, 262 insertions(+), 126 deletions(-) delete mode 100644 core/ui_library/example/lib/src/widget/theme_switcher.dart create mode 100644 core/ui_showcase/lib/src/widget/inputs/theme_input.dart create mode 100644 core/ui_showcase/lib/src/widget/restoration/page_storage_reader.dart diff --git a/core/ui_library/example/lib/main.dart b/core/ui_library/example/lib/main.dart index 172d737b..5fd017e3 100644 --- a/core/ui_library/example/lib/main.dart +++ b/core/ui_library/example/lib/main.dart @@ -30,7 +30,16 @@ class _MyAppState extends State { super.initState(); router = GoRouter( routes: [ - createRootShowcaseRoute(showcaseNodes), + createRootShowcaseRoute( + nodes: showcaseNodes, + builder: (context, child) => ThemeOptionProvider( + options: [ + ThemeOption(name: 'Light', theme: ThemeData.light()), + ThemeOption(name: 'Dark', theme: ThemeData.dark()), + ], + child: child, + ), + ), ], ); } diff --git a/core/ui_library/example/lib/src/widget/component_preview.dart b/core/ui_library/example/lib/src/widget/component_preview.dart index b171428f..9930cc41 100644 --- a/core/ui_library/example/lib/src/widget/component_preview.dart +++ b/core/ui_library/example/lib/src/widget/component_preview.dart @@ -1,5 +1,5 @@ import 'package:example/src/widget/sidebar.dart'; -import 'package:example/src/widget/theme_switcher.dart'; +import 'package:ui_showcase/src/widget/inputs/theme_input.dart'; import 'package:flutter/material.dart'; import 'package:ui_showcase/ui_showcase.dart'; @@ -14,18 +14,14 @@ class ComponentPreview extends StatelessWidget { final WidgetBuilder builder; @override - Widget build(BuildContext context) => ThemeOptionInherited( - builder: (context, theme) { - return ShowcaseNodePreview( - builder: builder, - listenables: inputs.map((e) => e.listenable).toList(growable: false), - sidebar: Sidebar(children: inputs), - wrapWith: (child) { - return MaterialApp( - theme: theme, - home: Scaffold(body: Center(child: child)), - ); - }, + Widget build(BuildContext context) => ShowcaseNodePreview( + builder: builder, + listenables: inputs.map((e) => e.listenable).toList(growable: false), + sidebar: Sidebar(children: inputs), + wrapWith: (child) { + return MaterialApp( + theme: ThemeOptionProvider.of(context).theme, + home: Scaffold(body: Center(child: child)), ); }, ); diff --git a/core/ui_library/example/lib/src/widget/sidebar.dart b/core/ui_library/example/lib/src/widget/sidebar.dart index 05f14ad6..39aad3e6 100644 --- a/core/ui_library/example/lib/src/widget/sidebar.dart +++ b/core/ui_library/example/lib/src/widget/sidebar.dart @@ -1,5 +1,5 @@ -import 'package:example/src/widget/theme_switcher.dart'; import 'package:flutter/material.dart'; +import 'package:ui_showcase/ui_showcase.dart'; class Sidebar extends StatelessWidget { const Sidebar({ diff --git a/core/ui_library/example/lib/src/widget/theme_switcher.dart b/core/ui_library/example/lib/src/widget/theme_switcher.dart deleted file mode 100644 index 300c6958..00000000 --- a/core/ui_library/example/lib/src/widget/theme_switcher.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; - -final lightTheme = ThemeData.light(); -final darkTheme = ThemeData.dark(); - -class ThemeOptionInput extends StatelessWidget { - const ThemeOptionInput({super.key}); - - static final _entries = >[ - DropdownMenuEntry(value: lightTheme, label: 'Light'), - DropdownMenuEntry(value: darkTheme, label: 'Dark'), - ]; - - @override - Widget build(BuildContext context) { - final theme = ThemeOptionInherited.of(context); - - return DropdownMenu( - dropdownMenuEntries: _entries, - initialSelection: theme ?? _entries.first.value, - onSelected: (value) { - if (value == null) return; - ThemeOptionInherited.setTheme(context, value); - }, - ); - } -} - -class ThemeOptionInherited extends StatefulWidget { - const ThemeOptionInherited({this.child, this.builder, super.key}); - - final Widget? child; - final Widget Function(BuildContext context, ThemeData theme)? builder; - - static void setTheme(BuildContext context, ThemeData theme) { - final inherited = - context.getElementForInheritedWidgetOfExactType<_ThemeOptionInherited>()?.widget - as _ThemeOptionInherited?; - inherited?.state.setTheme(theme); - } - - static ThemeData? of(BuildContext context, {bool listen = true}) { - final inherited = listen - ? context.dependOnInheritedWidgetOfExactType<_ThemeOptionInherited>() - : context.getElementForInheritedWidgetOfExactType<_ThemeOptionInherited>()?.widget - as _ThemeOptionInherited?; - return inherited?.theme; - } - - @override - State createState() => ThemeOptionInheritedState(); -} - -class ThemeOptionInheritedState extends State { - ThemeData _theme = lightTheme; - - void setTheme(ThemeData theme) { - if (_theme != theme) { - setState(() { - _theme = theme; - }); - } - } - - @override - Widget build(BuildContext context) { - return _ThemeOptionInherited( - theme: _theme, - state: this, - child: widget.builder?.call(context, _theme) ?? widget.child!, - ); - } -} - -class _ThemeOptionInherited extends InheritedWidget { - const _ThemeOptionInherited({required this.theme, required super.child, required this.state}); - - final ThemeOptionInheritedState state; - final ThemeData theme; - - @override - bool updateShouldNotify(_ThemeOptionInherited oldWidget) { - return theme != oldWidget.theme; - } -} diff --git a/core/ui_showcase/lib/src/routing/go_router.dart b/core/ui_showcase/lib/src/routing/go_router.dart index 38b298ef..a1994e02 100644 --- a/core/ui_showcase/lib/src/routing/go_router.dart +++ b/core/ui_showcase/lib/src/routing/go_router.dart @@ -2,7 +2,10 @@ import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:ui_showcase/ui_showcase.dart'; -RouteBase createRootShowcaseRoute(List nodes) { +RouteBase createRootShowcaseRoute({ + required List nodes, + Widget Function(BuildContext context, Widget child)? builder, +}) { ShowcaseNodes(nodes).assignParents(); return ShellRoute( @@ -13,10 +16,18 @@ RouteBase createRootShowcaseRoute(List nodes) { routes: _createRoutes(nodes), ), ], - builder: (context, state, child) => GoRouterNotifier( - nodes: nodes, - child: ShowcaseView(nodes: nodes, navigator: child), - ), + builder: (context, state, child) { + return GoRouterNotifier( + nodes: nodes, + child: Builder( + builder: (context) { + final showcaseView = ShowcaseView(nodes: nodes, navigator: child); + + return builder?.call(context, showcaseView) ?? showcaseView; + }, + ), + ); + }, ); } @@ -53,7 +64,7 @@ class NodeRouterGoRouter extends ValueNotifier implements NodeRou void _onLocationChanged() { final path = _router.state.fullPath ?? ''; - final node = ShowcaseNodes(_nodes).findNodeByPath(path); + final node = ShowcaseNodes(_nodes).findNodeByPath(path.replaceFirst('/', '')); value = node; } diff --git a/core/ui_showcase/lib/src/widget/inputs/boolean_input.dart b/core/ui_showcase/lib/src/widget/inputs/boolean_input.dart index e5d4d8d3..64c4f7a1 100644 --- a/core/ui_showcase/lib/src/widget/inputs/boolean_input.dart +++ b/core/ui_showcase/lib/src/widget/inputs/boolean_input.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:ui_showcase/src/widget/restoration/page_storage_reader.dart'; import 'package:ui_showcase/ui_showcase.dart'; -class BooleanInput extends StatelessWidget with InputWidget { +class BooleanInput extends StatefulWidget with InputWidget { const BooleanInput({ required this.label, required this.notifier, @@ -16,6 +17,28 @@ class BooleanInput extends StatelessWidget with InputWidget { @override Listenable get listenable => notifier; + @override + State createState() => _BooleanInputState(); +} + +class _BooleanInputState extends State with PageStorageReader { + @override + Object? obtainPageStorageIdentifier() { + final node = ActiveNodeNotifier.of(context, listen: false).activeNode; + + return '${node?.fullPath}-${widget.label}-boolean-input'; + } + + @override + void restoreState(bool data) { + widget.notifier.value = data; + } + + void _onChanged(bool value) { + widget.notifier.value = value; + writeStoredData(value); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -26,17 +49,14 @@ class BooleanInput extends StatelessWidget with InputWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: textTheme.labelLarge), + Text(widget.label, style: textTheme.labelLarge), ValueListenableBuilder( - valueListenable: notifier, + valueListenable: widget.notifier, builder: (context, value, child) { - return Switch( - value: value, - onChanged: (value) => notifier.value = value, - ); + return Switch(value: value, onChanged: _onChanged); }, ), - if (description case final description?) + if (widget.description case final description?) Text( description, style: textTheme.bodySmall?.copyWith(color: colorScheme.secondary), diff --git a/core/ui_showcase/lib/src/widget/inputs/integer_input.dart b/core/ui_showcase/lib/src/widget/inputs/integer_input.dart index 83722e2f..2e611a96 100644 --- a/core/ui_showcase/lib/src/widget/inputs/integer_input.dart +++ b/core/ui_showcase/lib/src/widget/inputs/integer_input.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:ui_showcase/src/widget/restoration/page_storage_reader.dart'; import 'package:ui_showcase/ui_showcase.dart'; -class IntegerInput extends StatelessWidget with InputWidget { +class IntegerInput extends StatefulWidget with InputWidget { const IntegerInput({ required this.notifier, required this.label, @@ -19,6 +20,28 @@ class IntegerInput extends StatelessWidget with InputWidget { @override Listenable get listenable => notifier; + @override + State createState() => _IntegerInputState(); +} + +class _IntegerInputState extends State with PageStorageReader { + @override + Object? obtainPageStorageIdentifier() { + final node = ActiveNodeNotifier.of(context, listen: false).activeNode; + + return '${node?.fullPath}-${widget.label}-integer-input'; + } + + @override + void restoreState(int data) { + widget.notifier.value = data; + } + + void _onChange(double value) { + widget.notifier.value = value.toInt(); + writeStoredData(value.toInt()); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -30,25 +53,26 @@ class IntegerInput extends StatelessWidget with InputWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - label, + widget.label, style: textTheme.labelLarge?.copyWith(color: colorScheme.onSurface), ), ValueListenableBuilder( - valueListenable: notifier, + valueListenable: widget.notifier, builder: (context, value, child) { return SliderTheme( data: const SliderThemeData(showValueIndicator: ShowValueIndicator.always), child: Slider( value: value.toDouble(), - min: min?.toDouble() ?? 0, - max: max?.toDouble() ?? 100, + min: widget.min?.toDouble() ?? 0, + max: widget.max?.toDouble() ?? 100, label: value.toString(), - onChanged: (value) => notifier.value = value.toInt(), + onChanged: _onChange, + onChangeEnd: _onChange, ), ); }, ), - if (description case final description?) + if (widget.description case final description?) Text( description, style: textTheme.bodySmall?.copyWith(color: colorScheme.outline), diff --git a/core/ui_showcase/lib/src/widget/inputs/string_input.dart b/core/ui_showcase/lib/src/widget/inputs/string_input.dart index ee8ff721..2e44169e 100644 --- a/core/ui_showcase/lib/src/widget/inputs/string_input.dart +++ b/core/ui_showcase/lib/src/widget/inputs/string_input.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:ui_showcase/src/widget/restoration/page_storage_reader.dart'; import 'package:ui_showcase/ui_showcase.dart'; -class StringInput extends StatelessWidget with InputWidget { +class StringInput extends StatefulWidget with InputWidget { const StringInput({ required this.label, required this.controller, @@ -17,6 +18,23 @@ class StringInput extends StatelessWidget with InputWidget { @override Listenable get listenable => controller; + @override + State createState() => _StringInputState(); +} + +class _StringInputState extends State with PageStorageReader { + @override + Object? obtainPageStorageIdentifier() { + final node = ActiveNodeNotifier.of(context, listen: false).activeNode; + + return '${node?.fullPath}-${widget.label}-string-input'; + } + + @override + void restoreState(String data) { + widget.controller.text = data; + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -28,13 +46,14 @@ class StringInput extends StatelessWidget with InputWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( - controller: controller, + controller: widget.controller, + onChanged: writeStoredData, decoration: InputDecoration( - labelText: label, - hintText: hint, + labelText: widget.label, + hintText: widget.hint, ), ), - if (description case final description?) + if (widget.description case final description?) Text( description, style: textTheme.bodySmall?.copyWith(color: colorScheme.secondary), diff --git a/core/ui_showcase/lib/src/widget/inputs/theme_input.dart b/core/ui_showcase/lib/src/widget/inputs/theme_input.dart new file mode 100644 index 00000000..68117631 --- /dev/null +++ b/core/ui_showcase/lib/src/widget/inputs/theme_input.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; + +class ThemeOptionInput extends StatelessWidget { + const ThemeOptionInput({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ThemeOptionProvider.of(context); + + return DropdownMenu( + dropdownMenuEntries: theme.options + .map((e) => DropdownMenuEntry(value: e.theme, label: e.name)) + .toList(), + initialSelection: theme.theme, + onSelected: (value) { + if (value == null) return; + ThemeOptionProvider.setTheme(context, value); + }, + ); + } +} + +class ThemeOptionProvider extends StatefulWidget { + const ThemeOptionProvider({required this.options, this.child, this.builder, super.key}); + + final Widget? child; + final Widget Function(BuildContext context, ThemeData theme)? builder; + final List options; + + static void setTheme(BuildContext context, ThemeData theme) { + final inherited = + context.getElementForInheritedWidgetOfExactType<_ThemeOptionInherited>()?.widget + as _ThemeOptionInherited?; + inherited?.state.setTheme(theme); + } + + static ThemeOptionProviderState of(BuildContext context, {bool listen = true}) { + final inherited = listen + ? context.dependOnInheritedWidgetOfExactType<_ThemeOptionInherited>() + : context.getElementForInheritedWidgetOfExactType<_ThemeOptionInherited>()?.widget + as _ThemeOptionInherited?; + + if (inherited == null) { + throw FlutterError.fromParts([ + ErrorSummary('ThemeOptionProvider not found in context'), + ErrorDescription('ThemeOptionProvider is required to be a parent of the widget that uses it'), + ErrorHint('Make sure to wrap your widget in ThemeOptionProvider'), + ]); + } + + return inherited.state; + } + + @override + State createState() => ThemeOptionProviderState(); +} + +class ThemeOptionProviderState extends State { + late ThemeData theme = widget.options.first.theme; + List get options => widget.options; + + void setTheme(ThemeData theme) { + if (this.theme != theme) { + setState(() { + this.theme = theme; + }); + } + } + + @override + Widget build(BuildContext context) { + return _ThemeOptionInherited( + theme: theme, + state: this, + child: widget.builder?.call(context, theme) ?? widget.child!, + ); + } +} + +class _ThemeOptionInherited extends InheritedWidget { + const _ThemeOptionInherited({required this.theme, required super.child, required this.state}); + + final ThemeOptionProviderState state; + final ThemeData theme; + + @override + bool updateShouldNotify(_ThemeOptionInherited oldWidget) { + return theme != oldWidget.theme; + } +} + +class ThemeOption { + const ThemeOption({required this.name, required this.theme}); + + final String name; + final ThemeData theme; + + @override + bool operator ==(Object other) { + if (other is ThemeOption) { + return name == other.name && theme == other.theme; + } + return false; + } + + @override + int get hashCode => name.hashCode ^ theme.hashCode; +} diff --git a/core/ui_showcase/lib/src/widget/restoration/page_storage_reader.dart b/core/ui_showcase/lib/src/widget/restoration/page_storage_reader.dart new file mode 100644 index 00000000..dc315e66 --- /dev/null +++ b/core/ui_showcase/lib/src/widget/restoration/page_storage_reader.dart @@ -0,0 +1,33 @@ +import 'package:flutter/widgets.dart'; + +mixin PageStorageReader on State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + final data = readStoredData(); + + if (data case final data?) restoreState(data); + }); + } + + Object? obtainPageStorageIdentifier(); + + void restoreState(K data) {} + + K? readStoredData([BuildContext? context]) { + final ctx = context ?? this.context; + final identifier = obtainPageStorageIdentifier(); + + final data = PageStorage.maybeOf(ctx)?.readState(ctx, identifier: identifier); + + return data as K?; + } + + void writeStoredData(K data, [BuildContext? context]) { + final ctx = context ?? this.context; + final identifier = obtainPageStorageIdentifier(); + + PageStorage.maybeOf(ctx)?.writeState(ctx, data, identifier: identifier); + } +} diff --git a/core/ui_showcase/lib/ui_showcase.dart b/core/ui_showcase/lib/ui_showcase.dart index 3c885290..339105fd 100644 --- a/core/ui_showcase/lib/ui_showcase.dart +++ b/core/ui_showcase/lib/ui_showcase.dart @@ -5,6 +5,7 @@ export 'src/widget/inputs/boolean_input.dart'; export 'src/widget/inputs/input_widget.dart'; export 'src/widget/inputs/integer_input.dart'; export 'src/widget/inputs/string_input.dart'; +export 'src/widget/inputs/theme_input.dart'; export 'src/widget/showcase/active_node_notifier.dart'; export 'src/widget/showcase/showcase_node_preview.dart'; export 'src/widget/showcase/showcase_tree_view.dart'; From b173181eb0a380cb75b62ddbf0de384b78dda2ad Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Sat, 9 Aug 2025 15:27:38 +0200 Subject: [PATCH 19/22] Correctly save state --- .../lib/src/widget/inputs/boolean_input.dart | 11 +++++----- .../lib/src/widget/inputs/input_widget.dart | 2 +- .../lib/src/widget/inputs/integer_input.dart | 9 +++----- .../lib/src/widget/inputs/string_input.dart | 4 +++- .../restoration/page_storage_reader.dart | 21 ++++++++++++++++--- 5 files changed, 31 insertions(+), 16 deletions(-) diff --git a/core/ui_showcase/lib/src/widget/inputs/boolean_input.dart b/core/ui_showcase/lib/src/widget/inputs/boolean_input.dart index 64c4f7a1..227cd759 100644 --- a/core/ui_showcase/lib/src/widget/inputs/boolean_input.dart +++ b/core/ui_showcase/lib/src/widget/inputs/boolean_input.dart @@ -34,10 +34,8 @@ class _BooleanInputState extends State with PageStorageReader widget.notifier.value; @override Widget build(BuildContext context) { @@ -53,7 +51,10 @@ class _BooleanInputState extends State with PageStorageReader widget.notifier.value = value, + ); }, ), if (widget.description case final description?) diff --git a/core/ui_showcase/lib/src/widget/inputs/input_widget.dart b/core/ui_showcase/lib/src/widget/inputs/input_widget.dart index 3dfcd85f..48e7c501 100644 --- a/core/ui_showcase/lib/src/widget/inputs/input_widget.dart +++ b/core/ui_showcase/lib/src/widget/inputs/input_widget.dart @@ -1,5 +1,5 @@ import 'package:flutter/widgets.dart'; -mixin InputWidget on Widget { +mixin InputWidget on StatefulWidget { Listenable get listenable; } diff --git a/core/ui_showcase/lib/src/widget/inputs/integer_input.dart b/core/ui_showcase/lib/src/widget/inputs/integer_input.dart index 2e611a96..7fa468a4 100644 --- a/core/ui_showcase/lib/src/widget/inputs/integer_input.dart +++ b/core/ui_showcase/lib/src/widget/inputs/integer_input.dart @@ -37,10 +37,8 @@ class _IntegerInputState extends State with PageStorageReader widget.notifier.value; @override Widget build(BuildContext context) { @@ -66,8 +64,7 @@ class _IntegerInputState extends State with PageStorageReader widget.notifier.value = value.toInt(), ), ); }, diff --git a/core/ui_showcase/lib/src/widget/inputs/string_input.dart b/core/ui_showcase/lib/src/widget/inputs/string_input.dart index 2e44169e..7eca8ac3 100644 --- a/core/ui_showcase/lib/src/widget/inputs/string_input.dart +++ b/core/ui_showcase/lib/src/widget/inputs/string_input.dart @@ -35,6 +35,9 @@ class _StringInputState extends State with PageStorageReader widget.controller.text; + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -47,7 +50,6 @@ class _StringInputState extends State with PageStorageReader on State { +mixin PageStorageReader on State { @override void initState() { super.initState(); + widget.listenable.addListener(_onListenableChanged); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { final data = readStoredData(); @@ -11,9 +13,15 @@ mixin PageStorageReader on State { }); } - Object? obtainPageStorageIdentifier(); + @override + void dispose() { + widget.listenable.removeListener(_onListenableChanged); + super.dispose(); + } - void restoreState(K data) {} + K getCurrentValue(); + + Object? obtainPageStorageIdentifier(); K? readStoredData([BuildContext? context]) { final ctx = context ?? this.context; @@ -24,10 +32,17 @@ mixin PageStorageReader on State { return data as K?; } + void restoreState(K data) {} + void writeStoredData(K data, [BuildContext? context]) { final ctx = context ?? this.context; final identifier = obtainPageStorageIdentifier(); PageStorage.maybeOf(ctx)?.writeState(ctx, data, identifier: identifier); } + + void _onListenableChanged() { + final data = getCurrentValue(); + writeStoredData(data); + } } From 075107b385f5e05d46f1ab7eb66cbd6de56fafc6 Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Sat, 9 Aug 2025 15:37:32 +0200 Subject: [PATCH 20/22] Remove unused import for theme input in component preview --- core/ui_library/example/lib/src/widget/component_preview.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/core/ui_library/example/lib/src/widget/component_preview.dart b/core/ui_library/example/lib/src/widget/component_preview.dart index 9930cc41..d139e3fd 100644 --- a/core/ui_library/example/lib/src/widget/component_preview.dart +++ b/core/ui_library/example/lib/src/widget/component_preview.dart @@ -1,5 +1,4 @@ import 'package:example/src/widget/sidebar.dart'; -import 'package:ui_showcase/src/widget/inputs/theme_input.dart'; import 'package:flutter/material.dart'; import 'package:ui_showcase/ui_showcase.dart'; From fd4329a1c8eaebfde7720178228d7dabd54b9774 Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Sat, 9 Aug 2025 16:03:25 +0200 Subject: [PATCH 21/22] Add component preview sidebar functionality - Introduced `_ComponentPreviewStandard` for displaying a sidebar alongside the main content. - Updated `_ComponentPreviewSmall` to open the sidebar in a fullscreen dialog. - Removed redundant code and improved layout for better usability in the showcase view. --- .../showcase/showcase_node_preview.dart | 84 +++++++++++++------ .../src/widget/showcase/showcase_view.dart | 50 +++++------ 2 files changed, 84 insertions(+), 50 deletions(-) diff --git a/core/ui_showcase/lib/src/widget/showcase/showcase_node_preview.dart b/core/ui_showcase/lib/src/widget/showcase/showcase_node_preview.dart index 87c9483b..ab5a73c4 100644 --- a/core/ui_showcase/lib/src/widget/showcase/showcase_node_preview.dart +++ b/core/ui_showcase/lib/src/widget/showcase/showcase_node_preview.dart @@ -35,6 +35,41 @@ class ShowcaseNodePreview extends StatelessWidget { } } +class _ComponentPreviewStandard extends StatelessWidget { + const _ComponentPreviewStandard({ + required this.child, + required this.sidebar, + }); + + final Widget child; + final Widget? sidebar; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Row( + children: [ + Expanded(child: child), + if (sidebar != null) + DecoratedBox( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainer, + border: Border( + left: BorderSide(color: theme.colorScheme.outlineVariant), + ), + ), + child: SizedBox( + width: 200, + height: double.infinity, + child: sidebar, + ), + ), + ], + ); + } +} + class _ComponentPreviewSmall extends StatelessWidget { const _ComponentPreviewSmall({ required this.child, @@ -44,6 +79,16 @@ class _ComponentPreviewSmall extends StatelessWidget { final Widget? sidebar; final Widget child; + void _openSidebar(BuildContext context) { + showDialog( + context: context, + useRootNavigator: false, + builder: (context) { + return _SidebarFullscreenDialog(sidebar: sidebar!); + }, + ); + } + @override Widget build(BuildContext context) { return Stack( @@ -54,7 +99,7 @@ class _ComponentPreviewSmall extends StatelessWidget { right: 16, bottom: MediaQuery.paddingOf(context).bottom + 16, child: FloatingActionButton( - onPressed: () {}, + onPressed: () => _openSidebar(context), child: const Icon(Icons.edit_rounded), ), ), @@ -63,37 +108,26 @@ class _ComponentPreviewSmall extends StatelessWidget { } } -class _ComponentPreviewStandard extends StatelessWidget { - const _ComponentPreviewStandard({ - required this.child, +class _SidebarFullscreenDialog extends StatelessWidget { + const _SidebarFullscreenDialog({ required this.sidebar, }); - final Widget child; - final Widget? sidebar; + final Widget sidebar; @override Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Row( - children: [ - Expanded(child: child), - if (sidebar != null) - DecoratedBox( - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainer, - border: Border( - left: BorderSide(color: theme.colorScheme.outlineVariant), - ), - ), - child: SizedBox( - width: 200, - height: double.infinity, - child: sidebar, - ), + return Dialog.fullscreen( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close_rounded), ), - ], + Expanded(child: sidebar), + ], + ), ); } } diff --git a/core/ui_showcase/lib/src/widget/showcase/showcase_view.dart b/core/ui_showcase/lib/src/widget/showcase/showcase_view.dart index 54c53990..71a3624e 100644 --- a/core/ui_showcase/lib/src/widget/showcase/showcase_view.dart +++ b/core/ui_showcase/lib/src/widget/showcase/showcase_view.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:ui_showcase/src/widget/showcase/active_node_metadata.dart'; import 'package:ui_showcase/ui_showcase.dart'; /// A view that displays a list of showcase nodes. @@ -77,30 +76,31 @@ class ShowcaseMobile extends StatelessWidget { }, ), ), - body: Builder( - builder: (context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(4), - child: Row( - children: [ - IconButton( - icon: const Icon(Icons.menu_rounded), - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - ), - if (activeNode case final activeNode?) ActiveNodeMetadata(node: activeNode), - ], - ), - ), - Expanded(child: navigator), - ], - ); - }, - ), + body: navigator, + // body: Builder( + // builder: (context) { + // return Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Padding( + // padding: const EdgeInsets.all(4), + // child: Row( + // children: [ + // IconButton( + // icon: const Icon(Icons.menu_rounded), + // onPressed: () { + // Scaffold.of(context).openDrawer(); + // }, + // ), + // if (activeNode case final activeNode?) ActiveNodeMetadata(node: activeNode), + // ], + // ), + // ), + // Expanded(child: navigator), + // ], + // ); + // }, + // ), ); } } From 6fa5a44ba8f69947bbd0f368deb8f4424c1d2130 Mon Sep 17 00:00:00 2001 From: Michael Lazebny Date: Sun, 5 Oct 2025 21:04:03 +0200 Subject: [PATCH 22/22] chore: add ui_showcase to workspace Adds the ui_showcase package to the workspace for better dependency management. --- core/ui_showcase/pubspec.yaml | 1 + pubspec.lock | 16 ++++++++++++++++ pubspec.yaml | 2 ++ 3 files changed, 19 insertions(+) diff --git a/core/ui_showcase/pubspec.yaml b/core/ui_showcase/pubspec.yaml index a92f6518..3978364b 100644 --- a/core/ui_showcase/pubspec.yaml +++ b/core/ui_showcase/pubspec.yaml @@ -1,6 +1,7 @@ name: ui_showcase description: "A new Flutter package project." version: 0.0.1 +resolution: workspace environment: sdk: ^3.8.0 diff --git a/pubspec.lock b/pubspec.lock index c4b3f0e6..78ae3088 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -349,6 +349,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + go_router: + dependency: transitive + description: + name: go_router + sha256: c752e2d08d088bf83742cb05bf83003f3e9d276ff1519b5c92f9d5e60e5ddd23 + url: "https://pub.dev" + source: hosted + version: "16.2.4" graphs: dependency: transitive description: @@ -850,6 +858,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.6" + two_dimensional_scrollables: + dependency: transitive + description: + name: two_dimensional_scrollables + sha256: "0f77ecb96596f2f82eec2b0a8e60d9305c58315557da9fa3b610c7dbf5ded621" + url: "https://pub.dev" + source: hosted + version: "0.3.7" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b3af1f91..1ad05a4e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,9 @@ workspace: - core/rest_client - core/error_reporter - core/ui_library + - core/ui_showcase - core/database + # Feature - feature/settings - feature/home