From 7cf99524ec36bc18fdfbcd8cf951aeb4630ebbc3 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Thu, 29 Jan 2026 12:13:42 -0500 Subject: [PATCH 1/6] chore: add Flutter demo embedding to website - Add demo registry system with 250+ demo entries - Implement DartPadEmbed, FlutterEmbed, and FlutterMultiView React components - Create build script for Flutter web demos - Add web assets (favicon, icons, manifest) - Add comprehensive E2E tests for embedded demos - Update documentation with demo embedding guide and test pages - Integrate Playwright for visual regression testing --- .gitignore | 8 + examples/lib/demo_registry.dart | 387 +++++ examples/lib/main.dart | 243 +-- examples/lib/multi_view_app.dart | 86 + examples/lib/multi_view_stub.dart | 10 + examples/lib/multi_view_web.dart | 168 ++ examples/scripts/build_web_demos.sh | 185 +++ examples/web/favicon.png | Bin 0 -> 796 bytes examples/web/icons/Icon-192.png | Bin 0 -> 3697 bytes examples/web/icons/Icon-512.png | Bin 0 -> 10292 bytes examples/web/icons/Icon-maskable-192.png | Bin 0 -> 3697 bytes examples/web/icons/Icon-maskable-512.png | Bin 0 -> 10292 bytes examples/web/index.html | 169 ++ examples/web/manifest.json | 35 + website/components/DartPadEmbed.tsx | 245 +++ website/components/Demo.tsx | 224 +++ website/components/FlutterEmbed.tsx | 442 +++++ website/components/FlutterMultiView.tsx | 497 ++++++ website/components/flutter-types.ts | 152 ++ website/components/flutter/config.ts | 65 + .../flutter/hooks/useFlutterErrorCapture.ts | 74 + .../components/flutter/initialization-lock.ts | 52 + website/components/flutter/loader.ts | 106 ++ website/components/flutter/timeouts.ts | 66 + website/components/flutter/validate-src.ts | 116 ++ website/e2e/dartpad-embed.spec.ts | 73 + website/e2e/demo.spec.ts | 87 + website/e2e/flutter-embed.spec.ts | 87 + website/e2e/flutter-multiview.spec.ts | 86 + website/e2e/helpers/dartpad.ts | 10 + website/e2e/helpers/scroll-and-wait.ts | 15 + website/e2e/visual.spec.ts | 113 ++ .../dartpad-dark-chromium-darwin.png | Bin 0 -> 8610 bytes .../dartpad-light-chromium-darwin.png | Bin 0 -> 6987 bytes .../demo-dartpad-chromium-darwin.png | Bin 0 -> 12605 bytes .../flutter-iframe-chromium-darwin.png | Bin 0 -> 22619 bytes website/package.json | 6 +- website/playwright.config.ts | 40 + website/src/content/documentation/_meta.js | 4 + .../src/content/documentation/test/_meta.js | 6 + .../content/documentation/test/demo-test.mdx | 122 ++ .../documentation/test/multiview-test.mdx | 51 + .../src/content/documentation/widgets/box.mdx | 8 + website/src/mdx-components.js | 11 + website/yarn.lock | 1456 +++++++++-------- 45 files changed, 4573 insertions(+), 932 deletions(-) create mode 100644 examples/lib/demo_registry.dart create mode 100644 examples/lib/multi_view_app.dart create mode 100644 examples/lib/multi_view_stub.dart create mode 100644 examples/lib/multi_view_web.dart create mode 100644 examples/scripts/build_web_demos.sh create mode 100644 examples/web/favicon.png create mode 100644 examples/web/icons/Icon-192.png create mode 100644 examples/web/icons/Icon-512.png create mode 100644 examples/web/icons/Icon-maskable-192.png create mode 100644 examples/web/icons/Icon-maskable-512.png create mode 100644 examples/web/index.html create mode 100644 examples/web/manifest.json create mode 100644 website/components/DartPadEmbed.tsx create mode 100644 website/components/Demo.tsx create mode 100644 website/components/FlutterEmbed.tsx create mode 100644 website/components/FlutterMultiView.tsx create mode 100644 website/components/flutter-types.ts create mode 100644 website/components/flutter/config.ts create mode 100644 website/components/flutter/hooks/useFlutterErrorCapture.ts create mode 100644 website/components/flutter/initialization-lock.ts create mode 100644 website/components/flutter/loader.ts create mode 100644 website/components/flutter/timeouts.ts create mode 100644 website/components/flutter/validate-src.ts create mode 100644 website/e2e/dartpad-embed.spec.ts create mode 100644 website/e2e/demo.spec.ts create mode 100644 website/e2e/flutter-embed.spec.ts create mode 100644 website/e2e/flutter-multiview.spec.ts create mode 100644 website/e2e/helpers/dartpad.ts create mode 100644 website/e2e/helpers/scroll-and-wait.ts create mode 100644 website/e2e/visual.spec.ts create mode 100644 website/e2e/visual.spec.ts-snapshots/dartpad-dark-chromium-darwin.png create mode 100644 website/e2e/visual.spec.ts-snapshots/dartpad-light-chromium-darwin.png create mode 100644 website/e2e/visual.spec.ts-snapshots/demo-dartpad-chromium-darwin.png create mode 100644 website/e2e/visual.spec.ts-snapshots/flutter-iframe-chromium-darwin.png create mode 100644 website/playwright.config.ts create mode 100644 website/src/content/documentation/test/_meta.js create mode 100644 website/src/content/documentation/test/demo-test.mdx create mode 100644 website/src/content/documentation/test/multiview-test.mdx diff --git a/.gitignore b/.gitignore index fc2df532c..54c5d9157 100644 --- a/.gitignore +++ b/.gitignore @@ -116,3 +116,11 @@ website/public/_pagefind/ # Claude Code CLAUDE.md .claude/settings.local.json + +# Flutter web demos (generated via: cd examples && flutter build web) +# These are built and deployed separately, not committed to git +website/public/demos/ + +# Playwright +website/playwright-report/ +website/test-results/ diff --git a/examples/lib/demo_registry.dart b/examples/lib/demo_registry.dart new file mode 100644 index 000000000..4698e6013 --- /dev/null +++ b/examples/lib/demo_registry.dart @@ -0,0 +1,387 @@ +import 'package:flutter/widgets.dart'; + +import 'api/animation/implicit.curved.hover.dart' as hover_scale; +import 'api/animation/implicit.curved.scale.dart' as auto_scale; +import 'api/animation/implicit.spring.translate.dart' as spring_anim; +import 'api/animation/keyframe.switch.dart' as animated_switch; +import 'api/animation/phase.compress.dart' as tap_phase; +import 'api/context_variants/disabled.dart' as disabled; +import 'api/context_variants/focused.dart' as focused; +import 'api/context_variants/hovered.dart' as hovered; +import 'api/context_variants/on_dark_light.dart' as dark_light; +import 'api/context_variants/pressed.dart' as pressed; +import 'api/context_variants/responsive_size.dart' as responsive_size; +import 'api/context_variants/selected.dart' as selected; +import 'api/context_variants/selected_toggle.dart' as selected_toggle; +import 'api/design_tokens/theme_tokens.dart' as theme_tokens; +import 'api/gradients/gradient_linear.dart' as gradient_linear; +import 'api/gradients/gradient_radial.dart' as gradient_radial; +import 'api/gradients/gradient_sweep.dart' as gradient_sweep; +import 'api/text/text_directives.dart' as text_directives; +import 'api/widgets/box/gradient_box.dart' as gradient_box; +import 'api/widgets/box/simple_box.dart' as simple_box; +import 'api/widgets/hbox/icon_label_chip.dart' as icon_label_chip; +import 'api/widgets/icon/styled_icon.dart' as styled_icon; +import 'api/widgets/text/styled_text.dart' as styled_text; +import 'api/widgets/vbox/card_layout.dart' as card_layout; +import 'api/widgets/zbox/layered_boxes.dart' as layered_boxes; + +/// A demo entry with metadata for both multi-view embedding and gallery display. +class DemoEntry { + const DemoEntry({ + required this.id, + required this.title, + required this.description, + required this.category, + required this.builder, + }); + + /// Unique ID used for multi-view embedding (e.g., 'box-basic'). + final String id; + + /// Display title for the gallery (e.g., 'Box - Basic'). + final String title; + + /// Short description of what this demo shows. + final String description; + + /// Category for grouping in the gallery. + final String category; + + /// Widget builder function. + final WidgetBuilder builder; +} + +/// Registry for demo widgets used in both multi-view embedding and the gallery. +/// +/// Single source of truth for all demo definitions, preventing duplication +/// between multi-view mode and gallery mode. +/// +/// Usage from JavaScript (multi-view): +/// ```javascript +/// app.addView({ +/// hostElement: container, +/// initialData: { demoId: 'box-basic' } +/// }); +/// ``` +class DemoRegistry { + DemoRegistry._(); + + /// All available demos with full metadata. + static const String _widgets = 'Widgets'; + static const String _variants = 'Context Variants'; + static const String _gradients = 'Gradients'; + static const String _tokens = 'Design System'; + static const String _animations = 'Animations'; + + static final List _demos = [ + // Widget Examples + DemoEntry( + id: 'box-basic', + title: 'Box - Basic', + description: 'Simple red box with rounded corners', + category: _widgets, + builder: (_) => const simple_box.Example(), + ), + DemoEntry( + id: 'box-gradient', + title: 'Box - Gradient', + description: 'Box with gradient and shadow', + category: _widgets, + builder: (_) => const gradient_box.Example(), + ), + DemoEntry( + id: 'hbox-chip', + title: 'HBox - Horizontal Layout', + description: 'Horizontal flex container with icon and text', + category: _widgets, + builder: (_) => const icon_label_chip.Example(), + ), + DemoEntry( + id: 'vbox-card', + title: 'VBox - Vertical Layout', + description: 'Vertical flex container with styled elements', + category: _widgets, + builder: (_) => const card_layout.Example(), + ), + DemoEntry( + id: 'zbox-layers', + title: 'ZBox - Stack Layout', + description: 'Stacked boxes with different alignments', + category: _widgets, + builder: (_) => const layered_boxes.Example(), + ), + DemoEntry( + id: 'icon-styled', + title: 'Icon - Styled', + description: 'Styled icon with custom size and color', + category: _widgets, + builder: (_) => const styled_icon.Example(), + ), + DemoEntry( + id: 'text-styled', + title: 'Text - Styled', + description: 'Styled text with custom typography', + category: _widgets, + builder: (_) => const styled_text.Example(), + ), + DemoEntry( + id: 'text-directives', + title: 'Text - Directives', + description: 'Text transformations: uppercase, lowercase, capitalize, etc.', + category: _widgets, + builder: (_) => const text_directives.Example(), + ), + + // Context Variants + DemoEntry( + id: 'variant-hover', + title: 'Hover State', + description: 'Box that changes color on hover', + category: _variants, + builder: (_) => const hovered.Example(), + ), + DemoEntry( + id: 'variant-pressed', + title: 'Press State', + description: 'Box that changes color when pressed', + category: _variants, + builder: (_) => const pressed.Example(), + ), + DemoEntry( + id: 'variant-focused', + title: 'Focus State', + description: 'Boxes that change color when focused', + category: _variants, + builder: (_) => const focused.Example(), + ), + DemoEntry( + id: 'variant-selected', + title: 'Selected State', + description: 'Box that toggles selected state', + category: _variants, + builder: (_) => const selected.Example(), + ), + DemoEntry( + id: 'variant-disabled', + title: 'Disabled State', + description: 'Disabled box with grey color', + category: _variants, + builder: (_) => const disabled.Example(), + ), + DemoEntry( + id: 'variant-dark-light', + title: 'Dark/Light Theme', + description: 'Boxes that adapt to theme changes', + category: _variants, + builder: (_) => const dark_light.Example(), + ), + DemoEntry( + id: 'variant-selected-toggle', + title: 'Selected Toggle', + description: 'Beautiful toggle button with selected state', + category: _variants, + builder: (_) => const selected_toggle.Example(), + ), + DemoEntry( + id: 'variant-responsive', + title: 'Responsive Size', + description: 'Dynamic sizing based on screen width', + category: _variants, + builder: (_) => const responsive_size.Example(), + ), + + // Gradients + DemoEntry( + id: 'gradient-linear', + title: 'Linear Gradient', + description: 'Beautiful purple-to-pink gradient with shadow', + category: _gradients, + builder: (_) => const gradient_linear.Example(), + ), + DemoEntry( + id: 'gradient-radial', + title: 'Radial Gradient', + description: 'Orange radial gradient with focal points', + category: _gradients, + builder: (_) => const gradient_radial.Example(), + ), + DemoEntry( + id: 'gradient-sweep', + title: 'Sweep Gradient', + description: 'Colorful sweep gradient creating rainbow effect', + category: _gradients, + builder: (_) => const gradient_sweep.Example(), + ), + + // Design Tokens + DemoEntry( + id: 'tokens-theme', + title: 'Design Tokens', + description: 'Using design tokens for consistent styling', + category: _tokens, + builder: (_) => const theme_tokens.Example(), + ), + + // Animations + DemoEntry( + id: 'anim-hover-scale', + title: 'Hover Scale Animation', + description: 'Box that scales up smoothly when hovered', + category: _animations, + builder: (_) => const hover_scale.Example(), + ), + DemoEntry( + id: 'anim-auto-scale', + title: 'Auto Scale Animation', + description: 'Box that automatically scales on load', + category: _animations, + builder: (_) => const auto_scale.Example(), + ), + DemoEntry( + id: 'anim-tap-phase', + title: 'Tap Phase Animation', + description: 'Multi-phase animation triggered by tap', + category: _animations, + builder: (_) => const tap_phase.BlockAnimation(), + ), + DemoEntry( + id: 'anim-switch', + title: 'Animated Switch', + description: 'Toggle switch with phase-based animation', + category: _animations, + builder: (_) => const animated_switch.SwitchAnimation(), + ), + DemoEntry( + id: 'anim-spring', + title: 'Spring Animation', + description: 'Bouncy spring physics animation', + category: _animations, + builder: (_) => const spring_anim.Example(), + ), + ]; + + /// Index for fast ID lookup. + static final Map _byId = { + for (final demo in _demos) demo.id: demo, + }; + + /// All registered demos. + static List get all => _demos; + + /// Builds a widget for the given demo ID. + /// + /// Returns an error widget if the demo ID is not found or if the demo + /// widget throws during construction. + static Widget build(String? demoId, BuildContext context) { + if (demoId == null || demoId.isEmpty) { + return const _UnknownDemo(demoId: 'null'); + } + + final entry = _byId[demoId]; + if (entry == null) { + return _UnknownDemo(demoId: demoId); + } + + // Build with error handling for construction errors + try { + return entry.builder(context); + } catch (e, stackTrace) { + debugPrint('Demo "$demoId" construction error: $e'); + return _ErrorDemo(demoId: demoId, error: e, stackTrace: stackTrace); + } + } + + /// Returns a list of all available demo IDs. + static List get availableDemos => _byId.keys.toList(); + + /// Checks if a demo ID exists in the registry. + static bool hasDemo(String demoId) => _byId.containsKey(demoId); + + /// Gets a demo entry by ID. + static DemoEntry? getById(String demoId) => _byId[demoId]; +} + +/// Widget displayed when an unknown demo ID is requested. +class _UnknownDemo extends StatelessWidget { + const _UnknownDemo({required this.demoId}); + + final String demoId; + + @override + Widget build(BuildContext context) { + // Sanitize demoId to prevent excessively long strings + final sanitizedId = + demoId.length > 100 ? '${demoId.substring(0, 100)}...' : demoId; + + return Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'Unknown demo: $sanitizedId\n\n' + 'Available demos:\n' + '${DemoRegistry.availableDemos.join('\n')}', + textAlign: TextAlign.center, + style: const TextStyle( + color: Color(0xFFEF4444), + fontSize: 14, + ), + ), + ), + ); + } +} + +/// Widget displayed when a demo throws an error. +class _ErrorDemo extends StatelessWidget { + const _ErrorDemo({ + required this.demoId, + required this.error, + this.stackTrace, + }); + + final String demoId; + final Object error; + final StackTrace? stackTrace; + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + '⚠️ Demo Error', + style: TextStyle( + color: Color(0xFFEF4444), + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + 'Demo: $demoId', + style: const TextStyle( + color: Color(0xFF9CA3AF), + fontSize: 12, + ), + ), + const SizedBox(height: 8), + Text( + error.toString(), + textAlign: TextAlign.center, + style: const TextStyle( + color: Color(0xFFEF4444), + fontSize: 12, + ), + maxLines: 5, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + } +} diff --git a/examples/lib/main.dart b/examples/lib/main.dart index 06599c428..76f993f79 100644 --- a/examples/lib/main.dart +++ b/examples/lib/main.dart @@ -1,39 +1,23 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'api/animation/implicit.curved.hover.dart' as hover_scale; -import 'api/animation/implicit.curved.scale.dart' as auto_scale; -import 'api/animation/implicit.spring.translate.dart' as spring_anim; -import 'api/animation/keyframe.switch.dart' as animated_switch; -import 'api/animation/phase.compress.dart' as tap_phase; -import 'api/context_variants/disabled.dart' as disabled; -import 'api/context_variants/focused.dart' as focused; -import 'api/context_variants/hovered.dart' as hovered; -import 'api/context_variants/on_dark_light.dart' as dark_light; -import 'api/context_variants/pressed.dart' as pressed; -import 'api/context_variants/responsive_size.dart' as responsive_size; -import 'api/context_variants/selected.dart' as selected; -import 'api/context_variants/selected_toggle.dart' as selected_toggle; -// Animation examples have different class names, will be added separately -import 'api/design_tokens/theme_tokens.dart' as theme_tokens; -// Gradient examples -import 'api/gradients/gradient_linear.dart' as gradient_linear; -import 'api/gradients/gradient_radial.dart' as gradient_radial; -import 'api/gradients/gradient_sweep.dart' as gradient_sweep; -// Text examples -import 'api/text/text_directives.dart' as text_directives; -import 'api/widgets/box/gradient_box.dart' as gradient_box; -// Import all example widgets -import 'api/widgets/box/simple_box.dart' as simple_box; -import 'api/widgets/hbox/icon_label_chip.dart' as icon_label_chip; -import 'api/widgets/icon/styled_icon.dart' as styled_icon; -import 'api/widgets/text/styled_text.dart' as styled_text; -import 'api/widgets/vbox/card_layout.dart' as card_layout; -import 'api/widgets/zbox/layered_boxes.dart' as layered_boxes; import 'components/chip_button.dart'; import 'components/custom_scaffold.dart'; +import 'demo_registry.dart'; +import 'multi_view_app.dart'; + +// Conditional import for web-specific APIs +import 'multi_view_stub.dart' if (dart.library.js_interop) 'multi_view_web.dart' + as multi_view; void main() { - runApp(const MixExampleApp()); + // On web with multi-view mode, use runWidget for multiple views + // Otherwise use standard runApp for single view (including gallery mode) + if (kIsWeb && multi_view.isMultiViewEnabled) { + runWidget(const MultiViewApp()); + } else { + runApp(const MixExampleApp()); + } } class MixExampleApp extends StatelessWidget { @@ -65,189 +49,30 @@ class ExampleNavigator extends StatefulWidget { class _ExampleNavigatorState extends State { String _selectedCategory = 'All'; - final List _examples = [ - // Widget Examples - ExampleItem( - title: 'Box - Basic', - description: 'Simple red box with rounded corners', - category: 'Widgets', - widget: const simple_box.Example(), - ), - ExampleItem( - title: 'Box - Gradient', - description: 'Box with gradient and shadow', - category: 'Widgets', - widget: const gradient_box.Example(), - ), - ExampleItem( - title: 'HBox - Horizontal Layout', - description: 'Horizontal flex container with icon and text', - category: 'Widgets', - widget: const icon_label_chip.Example(), - ), - ExampleItem( - title: 'VBox - Vertical Layout', - description: 'Vertical flex container with styled elements', - category: 'Widgets', - widget: const card_layout.Example(), - ), - ExampleItem( - title: 'ZBox - Stack Layout', - description: 'Stacked boxes with different alignments', - category: 'Widgets', - widget: const layered_boxes.Example(), - ), - ExampleItem( - title: 'Icon - Styled', - description: 'Styled icon with custom size and color', - category: 'Widgets', - widget: const styled_icon.Example(), - ), - ExampleItem( - title: 'Text - Styled', - description: 'Styled text with custom typography', - category: 'Widgets', - widget: const styled_text.Example(), - ), - ExampleItem( - title: 'Text - Directives', - description: - 'Text transformations: uppercase, lowercase, capitalize, etc.', - category: 'Widgets', - widget: const text_directives.Example(), - ), - - // Context Variants - ExampleItem( - title: 'Hover State', - description: 'Box that changes color on hover', - category: 'Context Variants', - widget: const hovered.Example(), - ), - ExampleItem( - title: 'Press State', - description: 'Box that changes color when pressed', - category: 'Context Variants', - widget: const pressed.Example(), - ), - ExampleItem( - title: 'Focus State', - description: 'Boxes that change color when focused', - category: 'Context Variants', - widget: const focused.Example(), - ), - ExampleItem( - title: 'Selected State', - description: 'Box that toggles selected state', - category: 'Context Variants', - widget: const selected.Example(), - ), - ExampleItem( - title: 'Disabled State', - description: 'Disabled box with grey color', - category: 'Context Variants', - widget: const disabled.Example(), - ), - ExampleItem( - title: 'Dark/Light Theme', - description: 'Boxes that adapt to theme changes', - category: 'Context Variants', - widget: const dark_light.Example(), - ), - ExampleItem( - title: 'Selected Toggle', - description: 'Beautiful toggle button with selected state', - category: 'Context Variants', - widget: const selected_toggle.Example(), - ), - ExampleItem( - title: 'Responsive Size', - description: 'Dynamic sizing based on screen width', - category: 'Context Variants', - widget: const responsive_size.Example(), - ), - - // Gradients - ExampleItem( - title: 'Linear Gradient', - description: 'Beautiful purple-to-pink gradient with shadow', - category: 'Gradients', - widget: const gradient_linear.Example(), - ), - ExampleItem( - title: 'Radial Gradient', - description: 'Orange radial gradient with focal points', - category: 'Gradients', - widget: const gradient_radial.Example(), - ), - ExampleItem( - title: 'Sweep Gradient', - description: 'Colorful sweep gradient creating rainbow effect', - category: 'Gradients', - widget: const gradient_sweep.Example(), - ), + // Use centralized registry as single source of truth + List get _demos => DemoRegistry.all; - // Design Tokens - ExampleItem( - title: 'Design Tokens', - description: 'Using design tokens for consistent styling', - category: 'Design System', - widget: const theme_tokens.Example(), - ), - // Animations - ExampleItem( - title: 'Hover Scale Animation', - description: 'Box that scales up smoothly when hovered', - category: 'Animations', - widget: const hover_scale.Example(), - ), - ExampleItem( - title: 'Auto Scale Animation', - description: 'Box that automatically scales on load', - category: 'Animations', - widget: const auto_scale.Example(), - ), - ExampleItem( - title: 'Tap Phase Animation', - description: 'Multi-phase animation triggered by tap', - category: 'Animations', - widget: const tap_phase.BlockAnimation(), - ), - ExampleItem( - title: 'Animated Switch', - description: 'Toggle switch with phase-based animation', - category: 'Animations', - widget: const animated_switch.SwitchAnimation(), - ), - ExampleItem( - title: 'Spring Animation', - description: 'Bouncy spring physics animation', - category: 'Animations', - widget: const spring_anim.Example(), - ), - ]; - - Widget _buildExampleCard(ExampleItem example) { + Widget _buildExampleCard(DemoEntry demo, BuildContext context) { return Padding( padding: const .all(12), child: Column( crossAxisAlignment: .start, children: [ Text( - example.title, + demo.title, style: const TextStyle(fontSize: 16, fontWeight: .bold), overflow: .ellipsis, maxLines: 2, ), const SizedBox(height: 8), Text( - example.description, + demo.description, style: const TextStyle(color: Colors.grey, fontSize: 12), overflow: .ellipsis, maxLines: 2, ), const SizedBox(height: 12), - Expanded(child: Center(child: example.widget)), + Expanded(child: Center(child: demo.builder(context))), ], ), ); @@ -255,10 +80,10 @@ class _ExampleNavigatorState extends State { @override Widget build(BuildContext context) { - final categories = ['All', ..._examples.map((e) => e.category).toSet()]; - final filteredExamples = _selectedCategory == 'All' - ? _examples - : _examples.where((e) => e.category == _selectedCategory).toList(); + final categories = ['All', ..._demos.map((e) => e.category).toSet()]; + final filteredDemos = _selectedCategory == 'All' + ? _demos + : _demos.where((e) => e.category == _selectedCategory).toList(); return CustomScaffold( appBar: const CustomAppBar(title: 'Mix Examples'), @@ -295,11 +120,11 @@ class _ExampleNavigatorState extends State { childAspectRatio: 0.9, ), itemBuilder: (context, index) { - final example = filteredExamples[index]; + final demo = filteredDemos[index]; - return _buildExampleCard(example); + return _buildExampleCard(demo, context); }, - itemCount: filteredExamples.length, + itemCount: filteredDemos.length, ), ), ], @@ -307,17 +132,3 @@ class _ExampleNavigatorState extends State { ); } } - -class ExampleItem { - final String title; - final String description; - final String category; - final Widget widget; - - const ExampleItem({ - required this.title, - required this.description, - required this.category, - required this.widget, - }); -} diff --git a/examples/lib/multi_view_app.dart b/examples/lib/multi_view_app.dart new file mode 100644 index 000000000..58c323af0 --- /dev/null +++ b/examples/lib/multi_view_app.dart @@ -0,0 +1,86 @@ +import 'dart:ui' as ui; + +import 'package:flutter/widgets.dart'; + +import 'demo_registry.dart'; +import 'multi_view_stub.dart' if (dart.library.js_interop) 'multi_view_web.dart' + as multi_view; + +/// Multi-view app wrapper that renders different demos based on view's initialData. +/// +/// When embedded in a web page with multi-view enabled, each view receives +/// its own initialData containing the demoId to display. +/// +/// This widget implements the official Flutter multi-view pattern with +/// WidgetsBindingObserver to properly handle dynamic view additions/removals. +class MultiViewApp extends StatefulWidget { + const MultiViewApp({super.key}); + + @override + State createState() => _MultiViewAppState(); +} + +class _MultiViewAppState extends State with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeMetrics() { + // Rebuild when views are added or removed. + setState(() {}); + } + + @override + Widget build(BuildContext context) { + // Build a ViewCollection containing all active views. + final views = WidgetsBinding.instance.platformDispatcher.views; + + return ViewCollection( + views: [ + for (final view in views) + View( + view: view, + child: _DemoView(viewId: view.viewId), + ), + ], + ); + } +} + +/// Individual demo view widget that renders the appropriate demo +/// based on the initialData passed from JavaScript. +class _DemoView extends StatelessWidget { + const _DemoView({required this.viewId}); + + final int viewId; + + @override + Widget build(BuildContext context) { + // Get the initialData passed from JavaScript via addView(). + final initialData = multi_view.getInitialData(viewId); + // Defensive typing: JS may pass non-string values + final rawDemoId = initialData?['demoId']; + final demoId = rawDemoId is String ? rawDemoId : rawDemoId?.toString(); + final view = View.of(context); + + return ColoredBox( + color: const Color(0xFF1a1a2e), + child: Directionality( + textDirection: ui.TextDirection.ltr, + child: MediaQuery.fromView( + view: view, + child: DemoRegistry.build(demoId, context), + ), + ), + ); + } +} diff --git a/examples/lib/multi_view_stub.dart b/examples/lib/multi_view_stub.dart new file mode 100644 index 000000000..df907a5f1 --- /dev/null +++ b/examples/lib/multi_view_stub.dart @@ -0,0 +1,10 @@ +/// Stub implementation for non-web platforms. +/// +/// Multi-view mode is only supported on web, so these functions +/// return safe defaults on other platforms. + +/// Whether multi-view mode is enabled (always false on non-web). +bool get isMultiViewEnabled => false; + +/// Get initial data for a view (always null on non-web). +Map? getInitialData(int viewId) => null; diff --git a/examples/lib/multi_view_web.dart b/examples/lib/multi_view_web.dart new file mode 100644 index 000000000..5c5d96153 --- /dev/null +++ b/examples/lib/multi_view_web.dart @@ -0,0 +1,168 @@ +import 'dart:js_interop'; +import 'dart:ui_web' as ui_web; + +import 'package:flutter/foundation.dart'; + +/// Web implementation for multi-view mode support. +/// +/// These functions provide access to Flutter's multi-view APIs +/// that are only available on web. + +/// Maximum recursion depth for JS→Dart object conversion. +/// Configurable to handle different data structures. +const int kDefaultMaxConversionDepth = 10; + +/// Error thrown when JS→Dart conversion exceeds depth limit. +/// +/// This error is thrown instead of silently returning null when the +/// conversion depth exceeds [maxDepth]. This allows callers to distinguish +/// between null values and conversion failures. +class JsConversionDepthError implements Exception { + /// The depth at which the error occurred. + final int depth; + + /// The maximum allowed depth. + final int maxDepth; + + /// Creates a new [JsConversionDepthError]. + JsConversionDepthError(this.depth, this.maxDepth); + + @override + String toString() => + 'JsConversionDepthError: Exceeded max depth $maxDepth at depth $depth'; +} + +/// Check if the app was initialized with multi-view enabled. +/// +/// This is set by JavaScript via: +/// ```javascript +/// engineInitializer.initializeEngine({ multiViewEnabled: true }); +/// ``` +bool get isMultiViewEnabled { + try { + // Check if we have multiple views or if multi-view was explicitly enabled + // In multi-view mode, Flutter doesn't auto-create a default view + return _isMultiViewModeFromJS; + } catch (e) { + debugPrint('isMultiViewEnabled check failed: $e'); + return false; + } +} + +/// Get the initial data passed to a view via addView(). +/// +/// JavaScript can pass data when adding views: +/// ```javascript +/// app.addView({ +/// hostElement: container, +/// initialData: { demoId: 'box-basic' } +/// }); +/// ``` +/// +/// [maxDepth] controls the maximum recursion depth for nested objects. +/// Defaults to [kDefaultMaxConversionDepth] (10). +/// +/// Throws [JsConversionDepthError] if the object structure exceeds [maxDepth]. +/// Returns null only for null input or non-depth-related conversion failures. +Map? getInitialData(int viewId, {int? maxDepth}) { + try { + final jsData = ui_web.views.getInitialData(viewId); + if (jsData == null) return null; + + return _jsObjectToMap( + jsData, + maxDepth: maxDepth ?? kDefaultMaxConversionDepth, + ); + } on JsConversionDepthError { + // Re-throw depth errors for caller to handle + rethrow; + } catch (e) { + debugPrint('getInitialData failed for viewId $viewId: $e'); + return null; + } +} + +/// Convert a JSAny to a Dart Map. +/// +/// [maxDepth] - Maximum recursion depth (default: [kDefaultMaxConversionDepth]) +/// +/// Throws [JsConversionDepthError] if recursion exceeds maxDepth. +/// Returns null only for null input. +Map? _jsObjectToMap( + JSAny? jsAny, { + int depth = 0, + int maxDepth = kDefaultMaxConversionDepth, +}) { + if (jsAny == null) return null; + if (depth > maxDepth) { + debugPrint('_jsObjectToMap: Max depth $maxDepth exceeded at depth $depth'); + throw JsConversionDepthError(depth, maxDepth); + } + + final result = {}; + + // Get keys using Object.keys() + final jsObject = jsAny as JSObject; + final keys = _getObjectKeys(jsObject); + + for (final key in keys) { + final value = _getProperty(jsObject, key); + result[key] = _jsToValue(value, depth: depth + 1, maxDepth: maxDepth); + } + + return result; +} + +/// Convert a JSAny value to a Dart value. +/// +/// Throws [JsConversionDepthError] if recursion exceeds maxDepth. +Object? _jsToValue( + JSAny? jsValue, { + int depth = 0, + int maxDepth = kDefaultMaxConversionDepth, +}) { + if (jsValue == null) return null; + if (depth > maxDepth) { + throw JsConversionDepthError(depth, maxDepth); + } + if (jsValue.isA()) return (jsValue as JSString).toDart; + if (jsValue.isA()) return (jsValue as JSNumber).toDartDouble; + if (jsValue.isA()) return (jsValue as JSBoolean).toDart; + if (jsValue.isA()) { + return (jsValue as JSArray) + .toDart + .map((value) => _jsToValue(value, depth: depth + 1, maxDepth: maxDepth)) + .toList(); + } + if (jsValue.isA()) { + return _jsObjectToMap(jsValue, depth: depth + 1, maxDepth: maxDepth); + } + return jsValue.toString(); +} + +@JS('Object.keys') +external JSArray _objectKeys(JSObject obj); + +List _getObjectKeys(JSObject obj) { + return _objectKeys(obj).toDart.map((s) => s.toDart).toList(); +} + +@JS('Reflect.get') +external JSAny? _reflectGet(JSObject target, JSString key); + +JSAny? _getProperty(JSObject obj, String key) { + return _reflectGet(obj, key.toJS); +} + +/// Check from JS if multi-view mode was enabled. +@JS('window.__FLUTTER_MULTI_VIEW_ENABLED__') +external bool? get _multiViewEnabledFlag; + +bool get _isMultiViewModeFromJS { + try { + return _multiViewEnabledFlag ?? false; + } catch (e) { + debugPrint('_isMultiViewModeFromJS check failed: $e'); + return false; + } +} diff --git a/examples/scripts/build_web_demos.sh b/examples/scripts/build_web_demos.sh new file mode 100644 index 000000000..d228f6480 --- /dev/null +++ b/examples/scripts/build_web_demos.sh @@ -0,0 +1,185 @@ +#!/bin/bash +# Build Mix examples for web embedding +# +# Usage: +# ./scripts/build_web_demos.sh # Build and copy to website +# ./scripts/build_web_demos.sh --local # Build only (no copy) +# +# Output: +# - Build artifacts in examples/build/web/ +# - Copied to website/public/demos/ (unless --local) +# +# Note: The demos folder is gitignored. Run this script: +# - Locally before testing the website +# - In CI/CD before deploying the website + +# Exit immediately if any command fails (ensures build errors are not ignored) +set -e + +# ============================================================================ +# Directory Setup +# Derive all paths from the script's location to ensure consistent behavior +# regardless of where the script is invoked from. +# ============================================================================ +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EXAMPLES_DIR="$(dirname "$SCRIPT_DIR")" # examples/ directory +PROJECT_DIR="$(dirname "$EXAMPLES_DIR")" # project root (mix/) +BUILD_DIR="$EXAMPLES_DIR/build/web" # Flutter web build output +WEBSITE_DEMOS_DIR="$PROJECT_DIR/website/public/demos" # Website public folder + +# Parse command line arguments +LOCAL_ONLY=false +if [ "$1" = "--local" ]; then + LOCAL_ONLY=true +fi + +# Change to examples directory for Flutter commands +cd "$EXAMPLES_DIR" + +# Ensure Flutter is available +if ! command -v flutter &> /dev/null; then + echo "Error: Flutter not found. Please install Flutter or run setup.sh" + exit 1 +fi + +# Verify minimum Flutter version (3.38.0+ required for stable multi-view) +FLUTTER_VERSION=$(flutter --version | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') +REQUIRED_VERSION="3.38.0" + +version_ge() { + local IFS=. + local i ver1=($1) ver2=($2) + for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)); do + ver1[i]=0 + done + for ((i=0; i<${#ver1[@]}; i++)); do + if [[ -z ${ver2[i]} ]]; then + ver2[i]=0 + fi + if ((10#${ver1[i]} > 10#${ver2[i]})); then + return 0 + fi + if ((10#${ver1[i]} < 10#${ver2[i]})); then + return 1 + fi + done + return 0 +} + +if [ -z "$FLUTTER_VERSION" ]; then + echo "Error: Unable to parse Flutter version" + exit 1 +fi + +if ! version_ge "$FLUTTER_VERSION" "$REQUIRED_VERSION"; then + echo "Error: Flutter $REQUIRED_VERSION+ required, found $FLUTTER_VERSION" + exit 1 +fi + +echo "Building Mix examples for web..." +echo "Flutter version: $(flutter --version | head -1)" + +# ============================================================================ +# Build Process +# ============================================================================ + +# Clean previous builds to ensure fresh output (prevents stale artifacts) +rm -rf "$BUILD_DIR" + +# Build Flutter for web in release mode +# Uses CanvasKit renderer by default for best visual fidelity +# --pwa-strategy=none disables service worker generation to prevent 404 errors +# --base-href=/demos/ ensures all relative paths resolve correctly when served from /demos/ +flutter build web --release --pwa-strategy=none --base-href=/demos/ + +# Remove canvaskit (loaded from Google CDN automatically) +# This reduces the build size from ~30MB to ~3MB +rm -rf "$BUILD_DIR/canvaskit" + +# ============================================================================ +# Extract build config for runtime use +# This allows FlutterEmbed.tsx to load config dynamically instead of hard-coding +# the engineRevision, which would drift when Flutter is upgraded. +# ============================================================================ +FLUTTER_JS="$BUILD_DIR/flutter.js" +CONFIG_OUTPUT="$BUILD_DIR/flutter-build-config.json" + +if [ -f "$FLUTTER_JS" ]; then + # Extract engineRevision from flutter.js + # The format is: engineRevision: "abc123..." + ENGINE_REVISION=$(grep -oE 'engineRevision:\s*"[^"]+"' "$FLUTTER_JS" | grep -oE '"[^"]+"' | tr -d '"' | head -1) + + if [ -z "$ENGINE_REVISION" ]; then + echo "Error: Could not extract engineRevision from flutter.js" + exit 1 + fi + + # Generate config JSON for runtime loading + cat > "$CONFIG_OUTPUT" << EOF +{ + "engineRevision": "$ENGINE_REVISION", + "builds": [ + { + "compileTarget": "dart2js", + "renderer": "canvaskit", + "mainJsPath": "main.dart.js" + } + ] +} +EOF + echo "Generated flutter-build-config.json (engineRevision: $ENGINE_REVISION)" +else + echo "Error: flutter.js not found in build output" + exit 1 +fi + +# ============================================================================ +# Patch flutter_bootstrap.js for multi-view embedding +# The default build auto-calls _flutter.loader.load() which prevents us from +# configuring entrypointBaseUrl for proper path resolution when embedding. +# We remove the auto-load call so FlutterMultiView.tsx can call it with config. +# ============================================================================ +BOOTSTRAP_FILE="$BUILD_DIR/flutter_bootstrap.js" +if [ -f "$BOOTSTRAP_FILE" ]; then + # Check if auto-load exists before patching + if grep -q "_flutter\.loader\.load();" "$BOOTSTRAP_FILE"; then + # Remove the trailing _flutter.loader.load(); call + # This allows FlutterMultiView to call load() with entrypointBaseUrl config + sed -i.bak 's/_flutter\.loader\.load();$/\/\/ Auto-load disabled for multi-view embedding/' "$BOOTSTRAP_FILE" + rm -f "$BOOTSTRAP_FILE.bak" + + # Verify patch succeeded + if grep -q "Auto-load disabled" "$BOOTSTRAP_FILE"; then + echo "Patched flutter_bootstrap.js for multi-view embedding" + else + echo "Error: Bootstrap patch verification failed" + exit 1 + fi + else + echo "Error: flutter_bootstrap.js structure changed - auto-load pattern not found" + echo "Multi-view embedding requires patching the auto-load call." + echo "Check if Flutter changed the bootstrap output format." + exit 1 + fi +else + echo "Error: flutter_bootstrap.js not found in build output" + exit 1 +fi + +echo "" +echo "Build complete! Output: $BUILD_DIR" +echo "Build size: $(du -sh "$BUILD_DIR" | cut -f1)" + +if [ "$LOCAL_ONLY" = false ]; then + echo "" + echo "Copying to website/public/demos/..." + rm -rf "$WEBSITE_DEMOS_DIR" + mkdir -p "$WEBSITE_DEMOS_DIR" + cp -r "$BUILD_DIR"/* "$WEBSITE_DEMOS_DIR/" + echo "Done! Demos ready at: $WEBSITE_DEMOS_DIR" +fi + +echo "" +echo "To test locally:" +echo " cd $PROJECT_DIR/website && npm run dev" +echo " Open http://localhost:3000/documentation/widgets/box" diff --git a/examples/web/favicon.png b/examples/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..7910c758877889c09049cf484ed638fe5cb0171d GIT binary patch literal 796 zcmeAS@N?(olHy`uVBq!ia0vp^3Lq@N1SHpV7(M`Tl0AZa85pY67#JE_7#My5g&JNk zFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV+uh|q7;r{>zXKFuFY)wsWq-xUFQlvYH>`UO zP)N1JHKHUXu_VKdAc7+P4Fm|B^bX&V?=85k&uNhhIb$jwj5 zOsmALA=rEIJ)i~+xD6$lxv9k^iMa*1^{~V>F)}bP{_%8i42d|L9J6oF?%&t-+Z4HH zu-#%@EGLn{-cyyZK-S<@V`JHY73>-p-zWJ8gF9S#_gD2fyvy&I zDtOv^S46#YNn!WL^Qkcw333fhPkHY0>|c6KGuv>RfvRzAqR7Fe0cOryJ2LeSyS0V$ z#Dr{^9wk@t_{W>$|J!r=E%}UFI$xfMGPHX9rC~~y&f`!0OzZy}dhjUm*ZltUQ^5J& zZdLtrfBtJtUKw`8LOhsvxAgUrV>%bJW-_>)c0Zq|`%~Pq!-j#e|81V`=*Ihbk zGShI<#wm@WkzsBQU*#Y7^v}%il1$nEV*m7C@2!5=B-DFr^)9E)>fzrY8J|xl zHWdYB8Ty=-|9bDre{m;W>6LF9_4(~K{Newx;c=;Y^M;&dS~iLsZx%!txOHzmwKn}r z#h*9NCe;fzUtt#baO2&yC;KfV8L!s+7%M6nS%+9ls@H4P6?Zy5|Gf3P_kU}?EPk7f z`&B>c_x-l0w?3T3$iK1Z*!E5ASK|zHtFDxHzv^sPllu|z=i80e&x$lx+ z(k}gb`sa^J{M+$;nOv^T0Vi%}`P=s={SaS1Ggs`$k`JCw@5-@l_TTzm<^TP~t+6JV z+_u~HtNzsQD>R?1-uxta)wEwT8Va`r+rMfIjla={FS)vn8JFCEnq1Ew7Y MPgg&ebxsLQ0L|A#_W%F@ literal 0 HcmV?d00001 diff --git a/examples/web/icons/Icon-192.png b/examples/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..a891f8f0fdfc82d14a3828c6fc7e681831c97ba9 GIT binary patch literal 3697 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 t_$_hZpWT1f;h!Y + + + + + + + + + + + + Mix Examples + + + + + +
+
+
+
+
+ + + + + diff --git a/examples/web/manifest.json b/examples/web/manifest.json new file mode 100644 index 000000000..1dfecd1e5 --- /dev/null +++ b/examples/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "Mix Examples", + "short_name": "Mix", + "start_url": ".", + "display": "standalone", + "background_color": "#1a1a2e", + "theme_color": "#a78bfa", + "description": "Interactive Flutter examples for the Mix styling framework", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/website/components/DartPadEmbed.tsx b/website/components/DartPadEmbed.tsx new file mode 100644 index 000000000..c88ce1009 --- /dev/null +++ b/website/components/DartPadEmbed.tsx @@ -0,0 +1,245 @@ +"use client"; + +import React, { useMemo, useState } from "react"; + +interface DartPadEmbedProps { + /** GitHub Gist ID containing the Dart code */ + gistId?: string; + /** Inline Dart code to embed (alternative to gistId) */ + code?: string; + /** Theme: 'dark' for exercises, 'light' for demos */ + theme?: "dark" | "light"; + /** Height of the embed in pixels */ + height?: number; + /** Whether to show the run button */ + run?: boolean; + /** Split percentage between code and output (0-100) */ + split?: number; + /** Whether to use Flutter mode (vs pure Dart) */ + flutter?: boolean; + /** Title shown above the embed */ + title?: string; +} + +/** + * DartPadEmbed - Embeds an interactive DartPad in documentation + * + * Best practices from Dart team: + * - Use light theme for demos (read-only examples) + * - Use dark theme for exercises (user should modify) + * + * @example With Gist ID + * ```tsx + * + * ``` + * + * @example With inline code + * ```tsx + * runApp(MyApp()); + * `} + * flutter + * /> + * ``` + */ +export function DartPadEmbed({ + gistId, + code, + theme = "dark", + height = 500, + run = true, + split = 50, + flutter = true, + title, +}: DartPadEmbedProps) { + const [status, setStatus] = useState<"loading" | "ready" | "error">("loading"); + + // Validate that at least one content source is provided + const hasContent = Boolean(gistId || code); + + const iframeSrc = useMemo(() => { + const baseUrl = flutter + ? "https://dartpad.dev/embed-flutter.html" + : "https://dartpad.dev/embed-dart.html"; + + const params = new URLSearchParams(); + params.set("theme", theme); + params.set("run", run.toString()); + params.set("split", split.toString()); + + if (gistId) { + params.set("id", gistId); + } + + return `${baseUrl}?${params.toString()}`; + }, [gistId, theme, run, split, flutter]); + + const [resolvedSrc, setResolvedSrc] = useState(null); + + // Ensure iframe loads after hydration to avoid missing the load event. + React.useEffect(() => { + setStatus("loading"); + setResolvedSrc(iframeSrc); + }, [iframeSrc]); + + const reloadIframe = React.useCallback(() => { + setResolvedSrc(null); + requestAnimationFrame(() => setResolvedSrc(iframeSrc)); + }, [iframeSrc]); + + // For inline code, we need to use a different approach + // DartPad supports postMessage API for setting code + const iframeRef = React.useRef(null); + + React.useEffect(() => { + if (code && iframeRef.current) { + const iframe = iframeRef.current; + + const sendCode = () => { + try { + if (!iframe.contentWindow) { + console.error("DartPadEmbed: iframe contentWindow not available"); + return; + } + iframe.contentWindow.postMessage( + { + sourceCode: code.trim(), + type: "sourceCode", + }, + "https://dartpad.dev" + ); + } catch (error) { + console.error("DartPadEmbed: Failed to post code to DartPad", error); + } + }; + + const retryTimeouts: Array> = []; + const sendCodeWithRetry = () => { + sendCode(); + retryTimeouts.push(setTimeout(sendCode, 500)); + retryTimeouts.push(setTimeout(sendCode, 1500)); + }; + + // Send immediately and also on load to handle cross-origin iframe readiness + sendCodeWithRetry(); + iframe.addEventListener("load", sendCodeWithRetry); + + return () => { + iframe.removeEventListener("load", sendCodeWithRetry); + retryTimeouts.forEach((timeoutId) => clearTimeout(timeoutId)); + }; + } + }, [code, resolvedSrc]); + + // Show error if no content source provided + if (!hasContent) { + return ( +
+ {title && ( +
{title}
+ )} +
+
+
Missing content source
+
+ Provide either gistId or code prop +
+
+
+
+ ); + } + + // Show error if iframe failed to load + if (status === "error") { + return ( +
+ {title && ( +
{title}
+ )} +
+
+
Failed to load DartPad
+
+ Unable to connect to dartpad.dev. Check your internet connection. +
+ +
+
+
+ ); + } + + return ( +
+ {title && ( +
{title}
+ )} +
+ {resolvedSrc && ( +