From 55b77ef76e744f4a5d6e8b2daaaadc57c1e6b617 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 25 Sep 2026 15:39:23 -0400 Subject: [PATCH 1/7] refactor(superdeck): route image Hero flights through flightShuttleBuilder --- .../lib/src/builtins/image_widget.dart | 4 +- .../builders/image_element_builder.dart | 6 +- .../lib/src/markdown/markdown_hero_mixin.dart | 4 +- .../lib/src/ui/widgets/hero_element.dart | 58 ++------------- .../lib/src/ui/widgets/image_hero_flight.dart | 41 ---------- .../ui/widgets/image_hero_flight_widget.dart | 74 +++++++++++++++++++ .../src/markdown/hero_transition_test.dart | 9 ++- .../src/ui/widgets/hero_element_test.dart | 31 ++++---- 8 files changed, 108 insertions(+), 119 deletions(-) delete mode 100644 packages/superdeck/lib/src/ui/widgets/image_hero_flight.dart create mode 100644 packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart diff --git a/packages/superdeck/lib/src/builtins/image_widget.dart b/packages/superdeck/lib/src/builtins/image_widget.dart index 4569a726..4fed2d07 100644 --- a/packages/superdeck/lib/src/builtins/image_widget.dart +++ b/packages/superdeck/lib/src/builtins/image_widget.dart @@ -10,7 +10,7 @@ import '../rendering/blocks/block_provider.dart'; import '../rendering/blocks/image_hero_positions.dart'; import '../ui/widgets/cache_image_widget.dart'; import '../ui/widgets/hero_element.dart'; -import '../ui/widgets/image_hero_flight.dart'; +import '../ui/widgets/image_hero_flight_widget.dart'; import '../ui/widgets/provider.dart'; import '../ui/widgets/resolved_asset_image.dart'; import '../utils/converters.dart'; @@ -171,7 +171,7 @@ class ImageWidget extends StatelessWidget { child: buildElementHero( tag: automaticImageHeroTag(block.imageHeroStart), child: image, - buildFlight: buildImageHeroFlight, + flightShuttleBuilder: buildImageHeroFlight, ), ); } diff --git a/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart b/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart index 27b3a5bc..38424ce1 100644 --- a/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart +++ b/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart @@ -8,7 +8,7 @@ import '../../rendering/blocks/image_hero_positions.dart'; import '../../ui/widgets/cache_image_widget.dart'; import '../../ui/widgets/error_widgets.dart'; import '../../ui/widgets/hero_element.dart'; -import '../../ui/widgets/image_hero_flight.dart'; +import '../../ui/widgets/image_hero_flight_widget.dart'; import '../../ui/widgets/provider.dart'; import '../../ui/widgets/resolved_asset_image.dart'; import '../../utils/uri_validator.dart'; @@ -106,7 +106,7 @@ class ImageElementBuilder extends MarkdownElementBuilder size: totalSize, flightImage: image, ), - buildFlight: buildImageHeroFlight, + flightShuttleBuilder: buildImageHeroFlight, ); } @@ -122,7 +122,7 @@ class ImageElementBuilder extends MarkdownElementBuilder child: imageWidget, heroTag: heroTag, heroData: ImageElement(spec: spec, uri: uri, size: totalSize), - buildFlight: buildImageHeroFlight, + flightShuttleBuilder: buildImageHeroFlight, ); }, styleSpec: styleSpec, diff --git a/packages/superdeck/lib/src/markdown/markdown_hero_mixin.dart b/packages/superdeck/lib/src/markdown/markdown_hero_mixin.dart index 3dc557cd..15a8fa7d 100644 --- a/packages/superdeck/lib/src/markdown/markdown_hero_mixin.dart +++ b/packages/superdeck/lib/src/markdown/markdown_hero_mixin.dart @@ -14,8 +14,7 @@ mixin MarkdownHeroMixin on MarkdownElementBuilder { required Widget child, required String? heroTag, required T heroData, - Widget Function(BuildContext, T, T, double)? buildFlight, - HeroFlightShuttleBuilder? flightShuttleBuilder, + required HeroFlightShuttleBuilder flightShuttleBuilder, }) { final shouldAnimate = heroTag != null && !SlideConfiguration.of(context).isStaticRendering; @@ -27,7 +26,6 @@ mixin MarkdownHeroMixin on MarkdownElementBuilder { child: buildElementHero( tag: heroTag, child: child, - buildFlight: buildFlight, flightShuttleBuilder: flightShuttleBuilder, ), ); diff --git a/packages/superdeck/lib/src/ui/widgets/hero_element.dart b/packages/superdeck/lib/src/ui/widgets/hero_element.dart index e4d18ac9..2e395bcb 100644 --- a/packages/superdeck/lib/src/ui/widgets/hero_element.dart +++ b/packages/superdeck/lib/src/ui/widgets/hero_element.dart @@ -32,7 +32,8 @@ class ImageElement { final Uri uri; final Size size; - /// Rendered endpoint when an image uses an asset cache or `@image` layout. + /// Reuses an endpoint that cannot be reconstructed from [uri] and [spec], + /// such as a resolved asset, decoded image, or scaled `@image` layout. final Widget? flightImage; const ImageElement({ @@ -88,31 +89,24 @@ class HeroElement extends InheritedWidget { } } -/// Generic helper to build a Hero widget with custom flight animation. +/// Generic helper to build a Hero widget with a custom flight shuttle. /// -/// This eliminates duplication across text/code/image element builders by -/// providing a single implementation of the Hero + flightShuttleBuilder pattern. +/// This shares the Hero placeholder behavior across text, code, and image +/// element builders. The supplied shuttle builder owns its flight animation. /// /// Usage: /// ```dart /// buildElementHero( /// tag: 'myHero', /// child: StyledText('content'), -/// buildFlight: (context, from, to, t) { -/// return StyledText( -/// lerpString(from.text, to.text, t), -/// styleSpec: StyleSpec(spec: from.spec.lerp(to.spec, t)), -/// ); -/// }, +/// flightShuttleBuilder: buildTextHeroFlight, /// ) /// ``` Widget buildElementHero({ required String tag, required Widget child, - Widget Function(BuildContext context, T from, T to, double t)? buildFlight, - HeroFlightShuttleBuilder? flightShuttleBuilder, + required HeroFlightShuttleBuilder flightShuttleBuilder, }) { - assert(buildFlight != null || flightShuttleBuilder != null); return Hero( tag: tag, // A redirected page transition may rebuild the shuttle while either @@ -124,43 +118,7 @@ Widget buildElementHero({ size: size, child: Offstage(child: TickerMode(enabled: false, child: child)), ), - flightShuttleBuilder: - flightShuttleBuilder ?? - ( - BuildContext flightContext, - Animation animation, - HeroFlightDirection flightDirection, - BuildContext fromHeroContext, - BuildContext toHeroContext, - ) { - final to = HeroElement.of(toHeroContext); - final from = HeroElement.maybeOf(fromHeroContext) ?? to; - - // The shuttle is built inside the Navigator's Overlay, which sits - // outside the route's Material and therefore exposes a different - // DefaultTextStyle than the slide. Text properties an element spec - // leaves unset (letterSpacing, leadingDistribution, ...) would - // otherwise resolve against the Overlay's bare default and make the - // text change size the instant the Hero hands off to/from the real - // widget. Re-apply the source slide's DefaultTextStyle so the - // shuttle resolves identically to the widget it stands in for. - final slideTextStyle = DefaultTextStyle.of(fromHeroContext).style; - - return AnimatedBuilder( - animation: animation, - builder: (context, _) => DefaultTextStyle.merge( - style: slideTextStyle, - child: buildFlight!( - context, - from, - to, - flightDirection == HeroFlightDirection.push - ? animation.value - : 1 - animation.value, - ), - ), - ); - }, + flightShuttleBuilder: flightShuttleBuilder, child: child, ); } diff --git a/packages/superdeck/lib/src/ui/widgets/image_hero_flight.dart b/packages/superdeck/lib/src/ui/widgets/image_hero_flight.dart deleted file mode 100644 index 782ca567..00000000 --- a/packages/superdeck/lib/src/ui/widgets/image_hero_flight.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:mix/mix.dart'; - -import 'cache_image_widget.dart'; -import 'hero_element.dart'; - -/// Moves an image between frames while blending different image sources. -Widget buildImageHeroFlight( - BuildContext context, - ImageElement from, - ImageElement to, - double t, -) { - final spec = from.spec.lerp(to.spec, t); - final blend = Curves.easeInOut.transform(t); - - Widget image(ImageElement element) => - element.flightImage ?? - CachedImage( - key: ValueKey(element.uri), - uri: element.uri, - styleSpec: StyleSpec(spec: spec), - ); - - // Hero's overlay already interpolates the measured endpoint rectangles. - // A second size tween from block dimensions is wrong for intrinsically sized - // images and images with only one explicit dimension. - return SizedBox.expand( - key: const ValueKey('image-hero-flight'), - child: - from.uri == to.uri && from.flightImage == null && to.flightImage == null - ? image(from) - : Stack( - fit: StackFit.expand, - children: [ - Opacity(opacity: 1 - blend, child: image(from)), - Opacity(opacity: blend, child: image(to)), - ], - ), - ); -} diff --git a/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart b/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart new file mode 100644 index 00000000..d668b8b0 --- /dev/null +++ b/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart @@ -0,0 +1,74 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix/mix.dart'; + +import 'cache_image_widget.dart'; +import 'hero_element.dart'; + +/// Builds an image shuttle from the Hero endpoints for either flight direction. +Widget buildImageHeroFlight( + BuildContext context, + Animation animation, + HeroFlightDirection direction, + BuildContext fromContext, + BuildContext toContext, +) { + final to = HeroElement.of(toContext); + final from = HeroElement.maybeOf(fromContext) ?? to; + return ImageHeroFlight( + from: from, + to: to, + animation: direction == HeroFlightDirection.push + ? animation + : ReverseAnimation(animation), + ); +} + +/// Moves an image between frames while blending different image sources. +class ImageHeroFlight extends StatelessWidget { + final ImageElement from; + final ImageElement to; + final Animation animation; + + const ImageHeroFlight({ + super.key, + required this.from, + required this.to, + required this.animation, + }); + + Widget _image(ImageElement element, ImageSpec spec) => + element.flightImage ?? + CachedImage( + key: ValueKey(element.uri), + uri: element.uri, + styleSpec: StyleSpec(spec: spec), + ); + + @override + Widget build(BuildContext context) => AnimatedBuilder( + animation: animation, + builder: (context, _) { + final t = animation.value; + final spec = from.spec.lerp(to.spec, t); + final blend = Curves.easeInOut.transform(t); + + // Hero's overlay already interpolates the measured endpoint rectangles. + // A second size tween from block dimensions is wrong for intrinsically sized + // images and images with only one explicit dimension. + final sameUri = from.uri == to.uri; + final endpoint = to.flightImage ?? from.flightImage; + return SizedBox.expand( + key: const ValueKey('image-hero-flight'), + child: sameUri + ? endpoint ?? _image(from, spec) + : Stack( + fit: StackFit.expand, + children: [ + Opacity(opacity: 1 - blend, child: _image(from, spec)), + Opacity(opacity: blend, child: _image(to, spec)), + ], + ), + ); + }, + ); +} diff --git a/packages/superdeck/test/src/markdown/hero_transition_test.dart b/packages/superdeck/test/src/markdown/hero_transition_test.dart index a254f43e..f32471cf 100644 --- a/packages/superdeck/test/src/markdown/hero_transition_test.dart +++ b/packages/superdeck/test/src/markdown/hero_transition_test.dart @@ -9,7 +9,7 @@ import 'package:superdeck/src/styling/components/slide.dart'; import 'package:superdeck/src/styling/default_style.dart'; import 'package:superdeck/src/ui/widgets/cache_image_widget.dart'; import 'package:superdeck/src/ui/widgets/hero_element.dart'; -import 'package:superdeck/src/ui/widgets/image_hero_flight.dart'; +import 'package:superdeck/src/ui/widgets/image_hero_flight_widget.dart'; import 'package:superdeck/src/ui/widgets/provider.dart'; import 'package:superdeck_core/superdeck_core.dart'; @@ -513,8 +513,11 @@ void main() { width: 180, height: 120, child: Builder( - builder: (context) => - buildImageHeroFlight(context, from, to, 0.5), + builder: (context) => ImageHeroFlight( + from: from, + to: to, + animation: const AlwaysStoppedAnimation(0.5), + ), ), ), ), diff --git a/packages/superdeck/test/src/ui/widgets/hero_element_test.dart b/packages/superdeck/test/src/ui/widgets/hero_element_test.dart index d1bd783c..000cf3a2 100644 --- a/packages/superdeck/test/src/ui/widgets/hero_element_test.dart +++ b/packages/superdeck/test/src/ui/widgets/hero_element_test.dart @@ -6,12 +6,8 @@ import 'package:superdeck/src/ui/widgets/hero_element.dart'; /// Regression coverage for the "text size pops at the start/end of a hero /// transition" bug. /// -/// `buildElementHero` renders the flight shuttle inside the Navigator's -/// `Overlay`, which sits outside the route's `Material` and so exposes a -/// different `DefaultTextStyle` than the slide. Text properties an element -/// spec leaves unset (`letterSpacing`, `leadingDistribution`, ...) resolve -/// against that ambient style, so the shuttle used to render the text at a -/// different size than the real widget — a visible pop at the flight handoff. +/// A flight shuttle must resolve its own text style because it is built inside +/// the Navigator's Overlay, outside the route's Material. void main() { // A heading-like style (mirrors default_style.dart): sets the obvious // properties but leaves letterSpacing / leadingDistribution unset so they @@ -31,17 +27,18 @@ void main() { tag: 'h', // Mirrors StyledText -> Text(text, style: spec.style). child: const Text(text, style: headingStyle, key: routeKey), - buildFlight: (context, from, to, t) { - // Mirrors TextElementBuilder._buildStableFlight at a flight endpoint: - // a bare Text.rich with no `style:` argument. - return const Text.rich( - TextSpan( - style: headingStyle, - children: [TextSpan(text: text)], - ), - key: shuttleKey, - ); - }, + flightShuttleBuilder: + (context, animation, direction, fromContext, toContext) { + // Mirrors TextElementBuilder._buildStableFlight at a flight endpoint. + return Text.rich( + TextSpan( + style: headingStyle, + children: [TextSpan(text: text)], + ), + style: DefaultTextStyle.of(fromContext).style, + key: shuttleKey, + ); + }, ), ); From dc10b260a5c7433de1b005d7b47c5318b304360a Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 25 Sep 2026 16:17:31 -0400 Subject: [PATCH 2/7] refactor(superdeck): colocate image Hero shuttle adapter --- .../lib/src/builtins/image_widget.dart | 2 +- .../builders/image_element_builder.dart | 4 +- .../ui/widgets/image_hero_flight_widget.dart | 38 +++++++++---------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/superdeck/lib/src/builtins/image_widget.dart b/packages/superdeck/lib/src/builtins/image_widget.dart index 4fed2d07..8c4616c8 100644 --- a/packages/superdeck/lib/src/builtins/image_widget.dart +++ b/packages/superdeck/lib/src/builtins/image_widget.dart @@ -171,7 +171,7 @@ class ImageWidget extends StatelessWidget { child: buildElementHero( tag: automaticImageHeroTag(block.imageHeroStart), child: image, - flightShuttleBuilder: buildImageHeroFlight, + flightShuttleBuilder: ImageHeroFlight.buildShuttle, ), ); } diff --git a/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart b/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart index 38424ce1..09e00ef9 100644 --- a/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart +++ b/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart @@ -106,7 +106,7 @@ class ImageElementBuilder extends MarkdownElementBuilder size: totalSize, flightImage: image, ), - flightShuttleBuilder: buildImageHeroFlight, + flightShuttleBuilder: ImageHeroFlight.buildShuttle, ); } @@ -122,7 +122,7 @@ class ImageElementBuilder extends MarkdownElementBuilder child: imageWidget, heroTag: heroTag, heroData: ImageElement(spec: spec, uri: uri, size: totalSize), - flightShuttleBuilder: buildImageHeroFlight, + flightShuttleBuilder: ImageHeroFlight.buildShuttle, ); }, styleSpec: styleSpec, diff --git a/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart b/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart index d668b8b0..0f626574 100644 --- a/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart +++ b/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart @@ -4,25 +4,6 @@ import 'package:mix/mix.dart'; import 'cache_image_widget.dart'; import 'hero_element.dart'; -/// Builds an image shuttle from the Hero endpoints for either flight direction. -Widget buildImageHeroFlight( - BuildContext context, - Animation animation, - HeroFlightDirection direction, - BuildContext fromContext, - BuildContext toContext, -) { - final to = HeroElement.of(toContext); - final from = HeroElement.maybeOf(fromContext) ?? to; - return ImageHeroFlight( - from: from, - to: to, - animation: direction == HeroFlightDirection.push - ? animation - : ReverseAnimation(animation), - ); -} - /// Moves an image between frames while blending different image sources. class ImageHeroFlight extends StatelessWidget { final ImageElement from; @@ -36,6 +17,25 @@ class ImageHeroFlight extends StatelessWidget { required this.animation, }); + /// Adapts Flutter's Hero endpoints and flight direction to this widget. + static Widget buildShuttle( + BuildContext context, + Animation animation, + HeroFlightDirection direction, + BuildContext fromContext, + BuildContext toContext, + ) { + final to = HeroElement.of(toContext); + final from = HeroElement.maybeOf(fromContext) ?? to; + return ImageHeroFlight( + from: from, + to: to, + animation: direction == HeroFlightDirection.push + ? animation + : ReverseAnimation(animation), + ); + } + Widget _image(ImageElement element, ImageSpec spec) => element.flightImage ?? CachedImage( From 86e4d3bc963f61006fce5f993bebc5fa4ff9fd98 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 25 Sep 2026 16:20:36 -0400 Subject: [PATCH 3/7] refactor(superdeck): use image flight constructor as shuttle --- .../lib/src/builtins/image_widget.dart | 2 +- .../builders/image_element_builder.dart | 4 +- .../ui/widgets/image_hero_flight_widget.dart | 19 ++++--- .../src/markdown/hero_transition_test.dart | 51 +++++++++++++++---- 4 files changed, 53 insertions(+), 23 deletions(-) diff --git a/packages/superdeck/lib/src/builtins/image_widget.dart b/packages/superdeck/lib/src/builtins/image_widget.dart index 8c4616c8..45e2871c 100644 --- a/packages/superdeck/lib/src/builtins/image_widget.dart +++ b/packages/superdeck/lib/src/builtins/image_widget.dart @@ -171,7 +171,7 @@ class ImageWidget extends StatelessWidget { child: buildElementHero( tag: automaticImageHeroTag(block.imageHeroStart), child: image, - flightShuttleBuilder: ImageHeroFlight.buildShuttle, + flightShuttleBuilder: ImageHeroFlight.new, ), ); } diff --git a/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart b/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart index 09e00ef9..1eeaed63 100644 --- a/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart +++ b/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart @@ -106,7 +106,7 @@ class ImageElementBuilder extends MarkdownElementBuilder size: totalSize, flightImage: image, ), - flightShuttleBuilder: ImageHeroFlight.buildShuttle, + flightShuttleBuilder: ImageHeroFlight.new, ); } @@ -122,7 +122,7 @@ class ImageElementBuilder extends MarkdownElementBuilder child: imageWidget, heroTag: heroTag, heroData: ImageElement(spec: spec, uri: uri, size: totalSize), - flightShuttleBuilder: ImageHeroFlight.buildShuttle, + flightShuttleBuilder: ImageHeroFlight.new, ); }, styleSpec: styleSpec, diff --git a/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart b/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart index 0f626574..8e1f00ef 100644 --- a/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart +++ b/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart @@ -10,15 +10,8 @@ class ImageHeroFlight extends StatelessWidget { final ImageElement to; final Animation animation; - const ImageHeroFlight({ - super.key, - required this.from, - required this.to, - required this.animation, - }); - - /// Adapts Flutter's Hero endpoints and flight direction to this widget. - static Widget buildShuttle( + /// Builds the shuttle directly from Flutter's Hero flight arguments. + factory ImageHeroFlight( BuildContext context, Animation animation, HeroFlightDirection direction, @@ -27,7 +20,7 @@ class ImageHeroFlight extends StatelessWidget { ) { final to = HeroElement.of(toContext); final from = HeroElement.maybeOf(fromContext) ?? to; - return ImageHeroFlight( + return ImageHeroFlight._( from: from, to: to, animation: direction == HeroFlightDirection.push @@ -36,6 +29,12 @@ class ImageHeroFlight extends StatelessWidget { ); } + const ImageHeroFlight._({ + required this.from, + required this.to, + required this.animation, + }); + Widget _image(ImageElement element, ImageSpec spec) => element.flightImage ?? CachedImage( diff --git a/packages/superdeck/test/src/markdown/hero_transition_test.dart b/packages/superdeck/test/src/markdown/hero_transition_test.dart index f32471cf..055096fc 100644 --- a/packages/superdeck/test/src/markdown/hero_transition_test.dart +++ b/packages/superdeck/test/src/markdown/hero_transition_test.dart @@ -232,6 +232,12 @@ void main() { await tester.pump(_transitionDuration ~/ 2); expect(tester.takeException(), isNull); + expect( + tester + .widget(find.byKey(const ValueKey('image-hero-flight'))) + .child, + isA(), + ); await tester.pumpAndSettle(); @@ -506,28 +512,53 @@ void main() { size: const Size(240, 540), flightImage: const ColoredBox(color: Colors.blue), ); + final fromKey = GlobalKey(); + final toKey = GlobalKey(); await tester.pumpWidget( MaterialApp( - home: Center( - child: SizedBox( - width: 180, - height: 120, - child: Builder( - builder: (context) => ImageHeroFlight( - from: from, - to: to, - animation: const AlwaysStoppedAnimation(0.5), + home: Column( + children: [ + HeroElement( + data: from, + child: Builder( + key: fromKey, + builder: (_) => const SizedBox.shrink(), ), ), - ), + HeroElement( + data: to, + child: Builder( + key: toKey, + builder: (_) => const SizedBox.shrink(), + ), + ), + ], ), ), ); + final shuttle = ImageHeroFlight( + toKey.currentContext!, + const AlwaysStoppedAnimation(0.5), + HeroFlightDirection.push, + fromKey.currentContext!, + toKey.currentContext!, + ); + await tester.pumpWidget( + MaterialApp( + home: Center(child: SizedBox(width: 180, height: 120, child: shuttle)), + ), + ); expect( tester.getSize(find.byKey(const ValueKey('image-hero-flight'))), const Size(180, 120), ); + expect( + tester + .widget(find.byKey(const ValueKey('image-hero-flight'))) + .child, + same(to.flightImage), + ); expect(tester.takeException(), isNull); }); From 5a48c59b1dd4a259875cca48a21f055b346e16a7 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 25 Sep 2026 16:24:09 -0400 Subject: [PATCH 4/7] docs(superdeck): describe image Hero flight behavior --- docs/guides/markdown-authoring.mdx | 5 +++++ docs/reference/deck-options.mdx | 4 +++- docs/reference/markdown-syntax.mdx | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/guides/markdown-authoring.mdx b/docs/guides/markdown-authoring.mdx index a013aa3b..90b0c314 100644 --- a/docs/guides/markdown-authoring.mdx +++ b/docs/guides/markdown-authoring.mdx @@ -281,6 +281,11 @@ Add class names to animate elements between slides: Elements with matching class names animate smoothly during slide transitions. +For images, matching sources remain continuous while different sources +cross-fade during the Hero flight. Standalone images can also participate in +automatic image transitions by position; see the [`animateImages`](../reference/deck-options#animateimages) +reference for configuration details. + --- ## Overflow diagnostics diff --git a/docs/reference/deck-options.mdx b/docs/reference/deck-options.mdx index 4a35f9b1..0719b2e0 100644 --- a/docs/reference/deck-options.mdx +++ b/docs/reference/deck-options.mdx @@ -219,7 +219,9 @@ Pairs standalone Markdown images and `@image` blocks by order across consecutive slides so they animate through Hero transitions. Images nested in blockquotes, alerts, or list items do not take part. Set it to `false` to turn automatic image transitions off for the whole deck. Explicit Markdown Hero tags, -such as `![Diagram](assets/roadmap.png) {.hero-visual}`, are unaffected. +such as `![Diagram](assets/roadmap.png) {.hero-visual}`, are unaffected. When +paired images share a source, the flight keeps one continuous image; different +sources cross-fade between the two endpoints. ```dart DeckOptions( diff --git a/docs/reference/markdown-syntax.mdx b/docs/reference/markdown-syntax.mdx index a7129769..3bd155a2 100644 --- a/docs/reference/markdown-syntax.mdx +++ b/docs/reference/markdown-syntax.mdx @@ -227,6 +227,6 @@ Use Markdown class markers to animate matching elements between slides. The firs Use the same tag for the element that should transition from one slide to the next. For example, one `{.heading}` on slide A can animate to one `{.heading}` on slide B. -Standalone Markdown images and `@image` blocks transition automatically by image order: the first image on one slide moves to the first on the next slide, the second to the second, and so on. This also blends different image sources. Use an explicit Markdown class marker when an image needs to match by a named identity instead of its position. Images without a partner on the adjacent slide render normally. Images nested in blockquotes, alerts, or list items do not take part, and `DeckOptions(animateImages: false)` turns automatic image transitions off for the whole deck. +Standalone Markdown images and `@image` blocks transition automatically by image order: the first image on one slide moves to the first on the next slide, the second to the second, and so on. Images with the same source keep one continuous image during the flight; different sources cross-fade between the two endpoints. Use an explicit Markdown class marker when an image needs to match by a named identity instead of its position. Images without a partner on the adjacent slide render normally. Images nested in blockquotes, alerts, or list items do not take part, and `DeckOptions(animateImages: false)` turns automatic image transitions off for the whole deck. Do not use the same tag twice on one slide. Each Hero tag must identify one element per slide, so use separate tags like `{.heading}` and `{.subheading}` when multiple elements should animate. From a3a21610e10a89c8b8b2e8ef8c234614094b5944 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 25 Sep 2026 16:45:25 -0400 Subject: [PATCH 5/7] style(demo): make image treatment example full bleed --- .../.superdeck/build_status.json | 5 +- .../layout_showcase/.superdeck/superdeck.json | 292 ++++---- .../.superdeck/superdeck_full.json | 696 +++++++++--------- demo/layout_showcase/slides.md | 16 +- 4 files changed, 504 insertions(+), 505 deletions(-) diff --git a/demo/layout_showcase/.superdeck/build_status.json b/demo/layout_showcase/.superdeck/build_status.json index 5d533521..49bbecf5 100644 --- a/demo/layout_showcase/.superdeck/build_status.json +++ b/demo/layout_showcase/.superdeck/build_status.json @@ -1,6 +1,5 @@ { "status": "success", - "timestamp": "2026-07-13T04:01:58.442714Z", - "slideCount": 13, - "error": null + "timestamp": "2026-09-25T20:42:22.110996Z", + "slideCount": 13 } \ No newline at end of file diff --git a/demo/layout_showcase/.superdeck/superdeck.json b/demo/layout_showcase/.superdeck/superdeck.json index 2e72ba89..5c930c2f 100644 --- a/demo/layout_showcase/.superdeck/superdeck.json +++ b/demo/layout_showcase/.superdeck/superdeck.json @@ -10,7 +10,7 @@ { "blocks": [ { - "content": "\n\n#### NATIVE AUTHORING / VISUAL PROOF\n\n# The system makes room.\n\n## Thirteen frames for layout, image control, and rich Markdown.\n\n**Sections. Blocks. Images. Type.** One coherent visual language.\n", + "type": "block", "align": "centerLeft", "flex": 5, "padding": { @@ -20,10 +20,10 @@ "left": 34.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### NATIVE AUTHORING / VISUAL PROOF\n\n# The system makes room.\n\n## Thirteen frames for layout, image control, and rich Markdown.\n\n**Sections. Blocks. Images. Type.** One coherent visual language.\n" }, { - "name": "image", + "type": "widget", "align": "center", "flex": 6, "margin": { @@ -39,7 +39,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "scale": 1.04 @@ -63,7 +63,7 @@ { "blocks": [ { - "content": "\n\n#### 01 / PREDICTABLE GEOMETRY\n\n## The layout model explains itself.\n", + "type": "block", "align": "centerLeft", "flex": 1, "padding": { @@ -73,7 +73,7 @@ "left": 24.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### 01 / PREDICTABLE GEOMETRY\n\n## The layout model explains itself.\n" } ], "flex": 1, @@ -83,22 +83,22 @@ { "blocks": [ { - "content": "\n\n### SECTION = HEIGHT\n\nSections stack **vertically**. Their flex values decide how much of the slide each row receives.\n", + "type": "block", "flex": 2, "scrollable": false, - "type": "block" + "content": "\n\n### SECTION = HEIGHT\n\nSections stack **vertically**. Their flex values decide how much of the slide each row receives.\n" }, { - "content": "\n\n### 2×\n\nThis block gets twice the width of its neighbor.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n### 2×\n\nThis block gets twice the width of its neighbor.\n" }, { - "content": "\n\n### 1×\n\nThe ratio stays stable as the canvas scales.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n### 1×\n\nThe ratio stays stable as the canvas scales.\n" } ], "align": "center", @@ -109,23 +109,23 @@ { "blocks": [ { - "content": "\n\n#### MARGIN\n\nSpace outside the framed block.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### MARGIN\n\nSpace outside the framed block.\n" }, { - "content": "\n\n#### PADDING\n\nSpace inside the frame. **Decoration stays between them**, so the box model remains visible and teachable.\n", + "type": "block", "flex": 2, "scrollable": false, - "type": "block" + "content": "\n\n#### PADDING\n\nSpace inside the frame. **Decoration stays between them**, so the box model remains visible and teachable.\n" }, { - "content": "#### ALIGN\n\nContent lands at any of nine anchor points.", + "type": "block", "align": "bottomRight", "flex": 1, "scrollable": false, - "type": "block" + "content": "#### ALIGN\n\nContent lands at any of nine anchor points." } ], "align": "center", @@ -146,11 +146,11 @@ { "blocks": [ { - "content": "\n\n#### 02 / SECTIONS + BLOCKS + IMAGES\n\n## Editorial, asymmetric, still deterministic.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 02 / SECTIONS + BLOCKS + IMAGES\n\n## Editorial, asymmetric, still deterministic.\n" } ], "flex": 1, @@ -160,7 +160,7 @@ { "blocks": [ { - "content": "\n\n### Build hierarchy before decoration.\n\nA strong composition begins with **one dominant relationship**. Here, a 4:7 split gives the image authority while the copy keeps a clear reading edge.\n\n- **Flex** sets the proportion.\n- **Spacing** creates the gutter.\n- **Alignment** pins the content.\n\n> The result feels art-directed because every constraint has a job.\n", + "type": "block", "align": "centerLeft", "flex": 4, "padding": { @@ -170,10 +170,10 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "\n\n### Build hierarchy before decoration.\n\nA strong composition begins with **one dominant relationship**. Here, a 4:7 split gives the image authority while the copy keeps a clear reading edge.\n\n- **Flex** sets the proportion.\n- **Spacing** creates the gutter.\n- **Alignment** pins the content.\n\n> The result feels art-directed because every constraint has a job.\n" }, { - "name": "image", + "type": "widget", "align": "centerRight", "flex": 7, "margin": { @@ -189,7 +189,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "scale": 1.12 @@ -203,7 +203,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "centerLeft", "flex": 2, "margin": { @@ -219,12 +219,12 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", "fit": "cover" }, { - "content": "\n\n#### SECONDARY RHYTHM\n\nRepeated image frames can change proportion without changing the rules around them.\n", + "type": "block", "align": "centerLeft", "flex": 4, "margin": { @@ -240,10 +240,10 @@ "left": 10.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### SECONDARY RHYTHM\n\nRepeated image frames can change proportion without changing the rules around them.\n" }, { - "name": "image", + "type": "widget", "align": "centerRight", "flex": 2, "margin": { @@ -259,7 +259,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/momentum_ribbon.png", "fit": "cover", "scale": 1.18 @@ -283,11 +283,11 @@ { "blocks": [ { - "content": "\n\n#### 03 / IMAGE FIT\n\n## Same source. Two honest choices.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 03 / IMAGE FIT\n\n## Same source. Two honest choices.\n" } ], "flex": 1, @@ -297,16 +297,16 @@ { "blocks": [ { - "content": "\n\n#### CONTAIN / FULL FRAME\n\n**Nothing is cropped.** Empty space is preserved.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### CONTAIN / FULL FRAME\n\n**Nothing is cropped.** Empty space is preserved.\n" }, { - "content": "\n\n#### COVER / FULL BLEED\n\n**The frame is filled.** Edges are intentionally cropped.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### COVER / FULL BLEED\n\n**The frame is filled.** Edges are intentionally cropped.\n" } ], "align": "center", @@ -317,7 +317,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "padding": { @@ -327,14 +327,14 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "contain", "width": 560, "height": 420 }, { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "padding": { @@ -344,7 +344,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "width": 560, @@ -369,11 +369,11 @@ { "blocks": [ { - "content": "\n\n#### 04 / SCALE + ALIGNMENT\n\n## Placement is a controlled variable.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 04 / SCALE + ALIGNMENT\n\n## Placement is a controlled variable.\n" } ], "flex": 1, @@ -383,7 +383,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "topLeft", "flex": 1, "padding": { @@ -393,13 +393,13 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "contain", "scale": 0.72 }, { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "padding": { @@ -409,13 +409,13 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "scale": 1 }, { - "name": "image", + "type": "widget", "align": "bottomRight", "flex": 1, "padding": { @@ -425,7 +425,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "scale": 1.35 @@ -439,7 +439,7 @@ { "blocks": [ { - "content": "\n\n#### 72% / TOP LEFT\n\nFull image, deliberate quiet space.\n", + "type": "block", "flex": 1, "margin": { "top": 14.0, @@ -448,10 +448,10 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### 72% / TOP LEFT\n\nFull image, deliberate quiet space.\n" }, { - "content": "\n\n#### 100% / CENTER\n\nThe reliable default crop.\n", + "type": "block", "flex": 1, "margin": { "top": 14.0, @@ -460,10 +460,10 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### 100% / CENTER\n\nThe reliable default crop.\n" }, { - "content": "#### 135% / BOTTOM RIGHT\n\nAn intentional detail view.", + "type": "block", "flex": 1, "margin": { "top": 14.0, @@ -472,7 +472,7 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "#### 135% / BOTTOM RIGHT\n\nAn intentional detail view." } ], "align": "topLeft", @@ -493,11 +493,11 @@ { "blocks": [ { - "content": "\n\n#### 05 / FRAME SIZE + BLOCK MARGIN\n\n## Frame size is authored, not guessed.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 05 / FRAME SIZE + BLOCK MARGIN\n\n## Frame size is authored, not guessed.\n" } ], "flex": 1, @@ -507,7 +507,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "margin": { @@ -523,13 +523,13 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", "fit": "cover", "scale": 1.06 }, { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "margin": { @@ -545,12 +545,12 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover" }, { - "name": "image", + "type": "widget", "align": "centerRight", "flex": 1, "margin": { @@ -566,7 +566,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/momentum_ribbon.png", "fit": "cover", "scale": 1.14 @@ -580,22 +580,22 @@ { "blocks": [ { - "content": "\n\n#### PORTRAIT / TALL\n\nHorizontal margin narrows the decorated image block.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### PORTRAIT / TALL\n\nHorizontal margin narrows the decorated image block.\n" }, { - "content": "\n\n#### SQUARE / BALANCED\n\nTwo-axis margin creates a compact focal tile.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### SQUARE / BALANCED\n\nTwo-axis margin creates a compact focal tile.\n" }, { - "content": "#### LANDSCAPE / WIDE\n\nVertical margin produces a cinematic banner.", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "#### LANDSCAPE / WIDE\n\nVertical margin produces a cinematic banner." } ], "align": "topLeft", @@ -607,7 +607,7 @@ "comments": [] }, { - "key": "UFjcGQIQ", + "key": "X18HmHk2", "options": { "title": "Image treatment matrix", "style": "compact" @@ -616,11 +616,11 @@ { "blocks": [ { - "content": "\n\n#### 06 / FIT + SURFACE TREATMENT\n\n## One image block can carry many behaviors.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 06 / FIT + SURFACE TREATMENT\n\n## One image block can carry many behaviors.\n" } ], "flex": 1, @@ -630,7 +630,7 @@ { "blocks": [ { - "content": "\n\n#### FIT WIDTH\n\n### Width leads.\n\nThe source fills the horizontal axis first.\n", + "type": "block", "align": "centerLeft", "flex": 2, "margin": { @@ -640,10 +640,10 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### FIT WIDTH\n\n### Width leads.\n\nThe source fills the horizontal axis first.\n" }, { - "name": "image", + "type": "widget", "align": "center", "flex": 4, "margin": { @@ -659,14 +659,14 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", "fit": "fitWidth", "width": 320, "height": 205 }, { - "content": "\n\n#### FIT HEIGHT\n\n### Height leads.\n\nPadding adds an inset inside the frame.\n", + "type": "block", "align": "centerLeft", "flex": 2, "margin": { @@ -676,10 +676,10 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### FIT HEIGHT\n\n### Height leads.\n\nPadding adds an inset inside the frame.\n" }, { - "name": "image", + "type": "widget", "align": "centerRight", "flex": 4, "margin": { @@ -695,7 +695,7 @@ "left": 12.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "fitHeight", "width": 320, @@ -710,7 +710,7 @@ { "blocks": [ { - "content": "\n\n#### FILL\n\n### Frame leads.\n\nPixels stretch to occupy both axes.\n", + "type": "block", "align": "centerLeft", "flex": 2, "margin": { @@ -720,10 +720,10 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### FILL\n\n### Frame leads.\n\nPixels stretch to occupy both axes.\n" }, { - "name": "image", + "type": "widget", "align": "center", "flex": 4, "margin": { @@ -739,14 +739,14 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/momentum_ribbon.png", "fit": "fill", "width": 320, "height": 205 }, { - "content": "\n\n#### SCALE DOWN\n\n### Source leads.\n\nMargin and padding create a floating study.\n", + "type": "block", "align": "centerLeft", "flex": 2, "margin": { @@ -756,31 +756,31 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### FULL BLEED\n\n### Frame leads.\n\nZero margin and padding let the image fill its card.\n" }, { - "name": "image", - "align": "bottomRight", + "type": "widget", + "align": "center", "flex": 4, "margin": { - "top": 16.0, - "right": 8.0, - "bottom": 16.0, - "left": 8.0 + "top": 0.0, + "right": 0.0, + "bottom": 0.0, + "left": 0.0 }, "padding": { - "top": 18.0, - "right": 18.0, - "bottom": 18.0, - "left": 18.0 + "top": 0.0, + "right": 0.0, + "bottom": 0.0, + "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", - "fit": "scaleDown", + "fit": "cover", "width": 320, "height": 205, - "scale": 0.82 + "scale": 1 } ], "align": "center", @@ -801,11 +801,11 @@ { "blocks": [ { - "content": "\n\n#### 07 / RICH MARKDOWN\n\n## The content model has range.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 07 / RICH MARKDOWN\n\n## The content model has range.\n" } ], "flex": 1, @@ -815,16 +815,16 @@ { "blocks": [ { - "content": "\n\n### Inline hierarchy\n\n**Bold establishes consequence.** *Italic adds a second voice.* ~~Strikethrough retires the old idea.~~ A [link creates a path](https://superdeck.dev), and `inline code` names the mechanism.\n\n> Typography can carry structure before a single custom widget enters the frame.\n", + "type": "block", "flex": 5, "scrollable": false, - "type": "block" + "content": "\n\n### Inline hierarchy\n\n**Bold establishes consequence.** *Italic adds a second voice.* ~~Strikethrough retires the old idea.~~ A [link creates a path](https://superdeck.dev), and `inline code` names the mechanism.\n\n> Typography can carry structure before a single custom widget enters the frame.\n" }, { - "content": "\n\n### List rhythm\n\n- **Lead** with the decision.\n- *Qualify* it with context.\n- Use `code` for exact syntax.\n\n1. Establish the frame.\n2. Repeat the cadence.\n3. Break the pattern once.\n", + "type": "block", "flex": 4, "scrollable": false, - "type": "block" + "content": "\n\n### List rhythm\n\n- **Lead** with the decision.\n- *Qualify* it with context.\n- Use `code` for exact syntax.\n\n1. Establish the frame.\n2. Repeat the cadence.\n3. Break the pattern once.\n" } ], "align": "topLeft", @@ -835,22 +835,22 @@ { "blocks": [ { - "content": "\n\n#### STRONG\n\n**Make the signal unmistakable.**\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### STRONG\n\n**Make the signal unmistakable.**\n" }, { - "content": "\n\n#### EMPHASIS\n\n*Change the tone without changing size.*\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### EMPHASIS\n\n*Change the tone without changing size.*\n" }, { - "content": "#### LINK + CODE\n\n[Open the docs](https://superdeck.dev) or use `@section`.", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "#### LINK + CODE\n\n[Open the docs](https://superdeck.dev) or use `@section`." } ], "align": "center", @@ -871,11 +871,11 @@ { "blocks": [ { - "content": "\n\n#### 08 / TABLES\n\n## Dense information can still breathe.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 08 / TABLES\n\n## Dense information can still breathe.\n" } ], "flex": 1, @@ -885,14 +885,14 @@ { "blocks": [ { - "content": "\n\n| Layout signal | Before | After | Movement |\n| :-- | --: | --: | --: |\n| Time to first read | 8.4 s | **2.6 s** | **−69%** |\n| Competing focal points | 7 | **2** | **−71%** |\n| Grid exceptions | 12 | **1** | **−92%** |\n| Review confidence | 54% | **91%** | **+37 pt** |\n", + "type": "block", "align": "centerLeft", "flex": 7, "scrollable": false, - "type": "block" + "content": "\n\n| Layout signal | Before | After | Movement |\n| :-- | --: | --: | --: |\n| Time to first read | 8.4 s | **2.6 s** | **−69%** |\n| Competing focal points | 7 | **2** | **−71%** |\n| Grid exceptions | 12 | **1** | **−92%** |\n| Review confidence | 54% | **91%** | **+37 pt** |\n" }, { - "content": "#### THE READING\n\n## 3.2×\n\nfaster orientation after the hierarchy was simplified.\n\n> A table earns attention when the comparison—not the border—is the story.", + "type": "block", "align": "centerLeft", "flex": 3, "padding": { @@ -902,7 +902,7 @@ "left": 10.0 }, "scrollable": false, - "type": "block" + "content": "#### THE READING\n\n## 3.2×\n\nfaster orientation after the hierarchy was simplified.\n\n> A table earns attention when the comparison—not the border—is the story." } ], "align": "center", @@ -923,11 +923,11 @@ { "blocks": [ { - "content": "\n\n#### 09 / GITHUB ALERTS\n\n## Guidance with the right degree of urgency.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 09 / GITHUB ALERTS\n\n## Guidance with the right degree of urgency.\n" } ], "flex": 1, @@ -937,16 +937,16 @@ { "blocks": [ { - "content": "\n\n> [!NOTE]\n> **Sections own height.** Blocks inside them divide the available width.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n> [!NOTE]\n> **Sections own height.** Blocks inside them divide the available width.\n" }, { - "content": "\n\n> [!TIP]\n> Reuse a stable spacing rhythm, then change only the value that carries meaning.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n> [!TIP]\n> Reuse a stable spacing rhythm, then change only the value that carries meaning.\n" } ], "align": "topLeft", @@ -957,22 +957,22 @@ { "blocks": [ { - "content": "\n\n> [!IMPORTANT]\n> Keep the focal relationship obvious at thumbnail size and at full screen.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n> [!IMPORTANT]\n> Keep the focal relationship obvious at thumbnail size and at full screen.\n" }, { - "content": "\n\n> [!WARNING]\n> Paint-only image scale can crop beyond its original fit. Review the final frame.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n> [!WARNING]\n> Paint-only image scale can crop beyond its original fit. Review the final frame.\n" }, { - "content": "> [!CAUTION]\n> Do not hide critical state in color alone. Pair the accent with a clear label.", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "> [!CAUTION]\n> Do not hide critical state in color alone. Pair the accent with a clear label." } ], "align": "topLeft", @@ -993,11 +993,11 @@ { "blocks": [ { - "content": "\n\n#### 10 / INDICATORS\n\n## Status should scan before it explains.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 10 / INDICATORS\n\n## Status should scan before it explains.\n" } ], "flex": 1, @@ -1007,16 +1007,16 @@ { "blocks": [ { - "content": "\n\n### Release readiness\n\n- [x] Layout contract locked\n- [x] Image crops reviewed\n- [x] Contrast checked at 1280 × 720\n- [ ] Speaker notes approved\n- [ ] Final export published\n\n> **3 / 5 complete.** The remaining work is visible without opening another tool.\n", + "type": "block", "flex": 5, "scrollable": false, - "type": "block" + "content": "\n\n### Release readiness\n\n- [x] Layout contract locked\n- [x] Image crops reviewed\n- [x] Contrast checked at 1280 × 720\n- [ ] Speaker notes approved\n- [ ] Final export published\n\n> **3 / 5 complete.** The remaining work is visible without opening another tool.\n" }, { - "content": "### Signal board\n\n| Workstream | State | Owner |\n| :-- | :-- | :-- |\n| Layout | **● READY** | Design |\n| Content | **● REVIEW** | Editorial |\n| Capture | **● READY** | QA |\n| Export | **○ QUEUED** | Release |\n\n**READY** means verified. *REVIEW* means a decision remains. `QUEUED` means the path is known.", + "type": "block", "flex": 5, "scrollable": false, - "type": "block" + "content": "### Signal board\n\n| Workstream | State | Owner |\n| :-- | :-- | :-- |\n| Layout | **● READY** | Design |\n| Content | **● REVIEW** | Editorial |\n| Capture | **● READY** | QA |\n| Export | **○ QUEUED** | Release |\n\n**READY** means verified. *REVIEW* means a decision remains. `QUEUED` means the path is known." } ], "align": "topLeft", @@ -1037,11 +1037,11 @@ { "blocks": [ { - "content": "\n\n#### 11 / NATIVE FIRST, EXTENSIBLE WHEN NEEDED\n\n## One escape hatch, clearly bounded.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 11 / NATIVE FIRST, EXTENSIBLE WHEN NEEDED\n\n## One escape hatch, clearly bounded.\n" } ], "flex": 1, @@ -1051,7 +1051,7 @@ { "blocks": [ { - "content": "\n\n#### BUILT-IN AUTHORING\n\n### Most of this deck is only three primitives.\n\n- `@section` for vertical rhythm\n- `@block` for Markdown content\n- `@image` for visual media\n\n**No custom content widget is required** for the layouts, tables, alerts, checklists, or image studies.\n", + "type": "block", "align": "centerLeft", "flex": 3, "padding": { @@ -1061,10 +1061,10 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### BUILT-IN AUTHORING\n\n### Most of this deck is only three primitives.\n\n- `@section` for vertical rhythm\n- `@block` for Markdown content\n- `@image` for visual media\n\n**No custom content widget is required** for the layouts, tables, alerts, checklists, or image studies.\n" }, { - "name": "image", + "type": "widget", "align": "center", "flex": 4, "padding": { @@ -1074,16 +1074,16 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", "fit": "cover", "scale": 1.08 }, { - "name": "showcaseMetric", + "type": "widget", "flex": 3, "scrollable": false, - "type": "widget", + "name": "showcaseMetric", "value": "01", "label": "Registered widget", "detail": "This single metric card is the deliberately custom extension point in the content deck.", @@ -1109,7 +1109,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "centerLeft", "flex": 4, "margin": { @@ -1125,13 +1125,13 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/momentum_ribbon.png", "fit": "cover", "scale": 1.24 }, { - "content": "#### 12 / THE OUTCOME\n\n# Built to be seen.\n\n## Every frame can be authored, inspected, and captured.\n\n**Native layouts. Rich Markdown. Controlled imagery.** One bounded widget extension and three original image assets.", + "type": "block", "align": "centerLeft", "flex": 5, "padding": { @@ -1141,7 +1141,7 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "#### 12 / THE OUTCOME\n\n# Built to be seen.\n\n## Every frame can be authored, inspected, and captured.\n\n**Native layouts. Rich Markdown. Controlled imagery.** One bounded widget extension and three original image assets." } ], "align": "center", diff --git a/demo/layout_showcase/.superdeck/superdeck_full.json b/demo/layout_showcase/.superdeck/superdeck_full.json index e742e901..c77c4d66 100644 --- a/demo/layout_showcase/.superdeck/superdeck_full.json +++ b/demo/layout_showcase/.superdeck/superdeck_full.json @@ -10,6 +10,16 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 5, + "padding": { + "top": 28.0, + "right": 34.0, + "bottom": 28.0, + "left": 34.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -70,20 +80,10 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 5, - "padding": { - "top": 28.0, - "right": 34.0, - "bottom": 28.0, - "left": 34.0 - }, - "scrollable": false, - "type": "block" + } }, { - "name": "image", + "type": "widget", "align": "center", "flex": 6, "margin": { @@ -99,7 +99,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "scale": 1.04 @@ -123,6 +123,16 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "padding": { + "top": 14.0, + "right": 24.0, + "bottom": 14.0, + "left": 24.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -152,17 +162,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "padding": { - "top": 14.0, - "right": 24.0, - "bottom": 14.0, - "left": 24.0 - }, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -172,6 +172,9 @@ { "blocks": [ { + "type": "block", + "flex": 2, + "scrollable": false, "content": { "type": "document", "children": [ @@ -214,12 +217,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 2, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -248,12 +251,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -282,10 +285,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "align": "center", @@ -296,6 +296,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -324,12 +327,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 2, + "scrollable": false, "content": { "type": "document", "children": [ @@ -372,12 +375,13 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 2, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "align": "bottomRight", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -406,11 +410,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "bottomRight", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "align": "center", @@ -431,6 +431,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -460,11 +464,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -474,6 +474,16 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 4, + "padding": { + "top": 0.0, + "right": 18.0, + "bottom": 0.0, + "left": 0.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -598,20 +608,10 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 4, - "padding": { - "top": 0.0, - "right": 18.0, - "bottom": 0.0, - "left": 0.0 - }, - "scrollable": false, - "type": "block" + } }, { - "name": "image", + "type": "widget", "align": "centerRight", "flex": 7, "margin": { @@ -627,7 +627,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "scale": 1.12 @@ -641,7 +641,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "centerLeft", "flex": 2, "margin": { @@ -657,11 +657,27 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", "fit": "cover" }, { + "type": "block", + "align": "centerLeft", + "flex": 4, + "margin": { + "top": 12.0, + "right": 0.0, + "bottom": 0.0, + "left": 0.0 + }, + "padding": { + "top": 0.0, + "right": 10.0, + "bottom": 0.0, + "left": 10.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -690,26 +706,10 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 4, - "margin": { - "top": 12.0, - "right": 0.0, - "bottom": 0.0, - "left": 0.0 - }, - "padding": { - "top": 0.0, - "right": 10.0, - "bottom": 0.0, - "left": 10.0 - }, - "scrollable": false, - "type": "block" + } }, { - "name": "image", + "type": "widget", "align": "centerRight", "flex": 2, "margin": { @@ -725,7 +725,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/momentum_ribbon.png", "fit": "cover", "scale": 1.18 @@ -749,6 +749,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -778,11 +782,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -792,6 +792,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -830,12 +833,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -874,10 +877,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "align": "center", @@ -888,7 +888,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "padding": { @@ -898,14 +898,14 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "contain", "width": 560, "height": 420 }, { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "padding": { @@ -915,7 +915,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "width": 560, @@ -940,6 +940,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -969,11 +973,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -983,7 +983,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "topLeft", "flex": 1, "padding": { @@ -993,13 +993,13 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "contain", "scale": 0.72 }, { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "padding": { @@ -1009,13 +1009,13 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "scale": 1 }, { - "name": "image", + "type": "widget", "align": "bottomRight", "flex": 1, "padding": { @@ -1025,7 +1025,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "scale": 1.35 @@ -1039,6 +1039,15 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "margin": { + "top": 14.0, + "right": 0.0, + "bottom": 0.0, + "left": 0.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1067,7 +1076,10 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, + } + }, + { + "type": "block", "flex": 1, "margin": { "top": 14.0, @@ -1076,9 +1088,6 @@ "left": 0.0 }, "scrollable": false, - "type": "block" - }, - { "content": { "type": "document", "children": [ @@ -1107,7 +1116,10 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, + } + }, + { + "type": "block", "flex": 1, "margin": { "top": 14.0, @@ -1116,9 +1128,6 @@ "left": 0.0 }, "scrollable": false, - "type": "block" - }, - { "content": { "type": "document", "children": [ @@ -1147,16 +1156,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "margin": { - "top": 14.0, - "right": 0.0, - "bottom": 0.0, - "left": 0.0 - }, - "scrollable": false, - "type": "block" + } } ], "align": "topLeft", @@ -1177,6 +1177,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1206,11 +1210,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -1220,7 +1220,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "margin": { @@ -1236,13 +1236,13 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", "fit": "cover", "scale": 1.06 }, { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "margin": { @@ -1258,12 +1258,12 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover" }, { - "name": "image", + "type": "widget", "align": "centerRight", "flex": 1, "margin": { @@ -1279,7 +1279,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/momentum_ribbon.png", "fit": "cover", "scale": 1.14 @@ -1293,6 +1293,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1321,12 +1324,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1355,12 +1358,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1389,10 +1392,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "align": "topLeft", @@ -1404,7 +1404,7 @@ "comments": [] }, { - "key": "UFjcGQIQ", + "key": "X18HmHk2", "options": { "title": "Image treatment matrix", "style": "compact" @@ -1413,6 +1413,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1442,11 +1446,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -1456,6 +1456,16 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 2, + "margin": { + "top": 16.0, + "right": 0.0, + "bottom": 16.0, + "left": 0.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1495,20 +1505,10 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 2, - "margin": { - "top": 16.0, - "right": 0.0, - "bottom": 16.0, - "left": 0.0 - }, - "scrollable": false, - "type": "block" + } }, { - "name": "image", + "type": "widget", "align": "center", "flex": 4, "margin": { @@ -1524,13 +1524,23 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", "fit": "fitWidth", "width": 320, "height": 205 }, { + "type": "block", + "align": "centerLeft", + "flex": 2, + "margin": { + "top": 16.0, + "right": 0.0, + "bottom": 16.0, + "left": 0.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1570,20 +1580,10 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 2, - "margin": { - "top": 16.0, - "right": 0.0, - "bottom": 16.0, - "left": 0.0 - }, - "scrollable": false, - "type": "block" + } }, { - "name": "image", + "type": "widget", "align": "centerRight", "flex": 4, "margin": { @@ -1599,7 +1599,7 @@ "left": 12.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "fitHeight", "width": 320, @@ -1614,6 +1614,16 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 2, + "margin": { + "top": 16.0, + "right": 0.0, + "bottom": 16.0, + "left": 0.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1653,20 +1663,10 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 2, - "margin": { - "top": 16.0, - "right": 0.0, - "bottom": 16.0, - "left": 0.0 - }, - "scrollable": false, - "type": "block" + } }, { - "name": "image", + "type": "widget", "align": "center", "flex": 4, "margin": { @@ -1682,13 +1682,23 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/momentum_ribbon.png", "fit": "fill", "width": 320, "height": 205 }, { + "type": "block", + "align": "centerLeft", + "flex": 2, + "margin": { + "top": 16.0, + "right": 0.0, + "bottom": 16.0, + "left": 0.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1698,10 +1708,10 @@ "children": [ { "type": "text", - "text": "SCALE DOWN" + "text": "FULL BLEED" } ], - "generatedId": "scale-down" + "generatedId": "full-bleed" }, { "type": "element", @@ -1709,10 +1719,10 @@ "children": [ { "type": "text", - "text": "Source leads." + "text": "Frame leads." } ], - "generatedId": "source-leads" + "generatedId": "frame-leads" }, { "type": "element", @@ -1720,7 +1730,7 @@ "children": [ { "type": "text", - "text": "Margin and padding create a floating study." + "text": "Zero margin and padding let the image fill its card." } ] } @@ -1728,41 +1738,31 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 2, - "margin": { - "top": 16.0, - "right": 0.0, - "bottom": 16.0, - "left": 0.0 - }, - "scrollable": false, - "type": "block" + } }, { - "name": "image", - "align": "bottomRight", + "type": "widget", + "align": "center", "flex": 4, "margin": { - "top": 16.0, - "right": 8.0, - "bottom": 16.0, - "left": 8.0 + "top": 0.0, + "right": 0.0, + "bottom": 0.0, + "left": 0.0 }, "padding": { - "top": 18.0, - "right": 18.0, - "bottom": 18.0, - "left": 18.0 + "top": 0.0, + "right": 0.0, + "bottom": 0.0, + "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", - "fit": "scaleDown", + "fit": "cover", "width": 320, "height": 205, - "scale": 0.82 + "scale": 1 } ], "align": "center", @@ -1783,6 +1783,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1812,11 +1816,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -1826,6 +1826,9 @@ { "blocks": [ { + "type": "block", + "flex": 5, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1939,12 +1942,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 5, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 4, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2069,10 +2072,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 4, - "scrollable": false, - "type": "block" + } } ], "align": "topLeft", @@ -2083,6 +2083,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2125,12 +2128,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2181,12 +2184,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2242,10 +2245,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "align": "center", @@ -2266,6 +2266,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2295,11 +2299,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -2309,6 +2309,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 7, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2680,13 +2684,19 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 7, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "align": "centerLeft", + "flex": 3, + "padding": { + "top": 0.0, + "right": 0.0, + "bottom": 0.0, + "left": 10.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2742,17 +2752,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 3, - "padding": { - "top": 0.0, - "right": 0.0, - "bottom": 0.0, - "left": 10.0 - }, - "scrollable": false, - "type": "block" + } } ], "align": "center", @@ -2773,6 +2773,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2802,11 +2806,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -2816,6 +2816,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2865,12 +2868,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2910,10 +2913,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "align": "topLeft", @@ -2924,6 +2924,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2963,12 +2966,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3008,12 +3011,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3053,10 +3056,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "align": "topLeft", @@ -3077,6 +3077,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3106,11 +3110,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -3120,6 +3120,9 @@ { "blocks": [ { + "type": "block", + "flex": 5, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3281,12 +3284,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 5, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 5, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3620,10 +3623,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 5, - "scrollable": false, - "type": "block" + } } ], "align": "topLeft", @@ -3644,6 +3644,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3673,11 +3677,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -3687,6 +3687,16 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 3, + "padding": { + "top": 0.0, + "right": 8.0, + "bottom": 0.0, + "left": 0.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3802,20 +3812,10 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 3, - "padding": { - "top": 0.0, - "right": 8.0, - "bottom": 0.0, - "left": 0.0 - }, - "scrollable": false, - "type": "block" + } }, { - "name": "image", + "type": "widget", "align": "center", "flex": 4, "padding": { @@ -3825,16 +3825,16 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", "fit": "cover", "scale": 1.08 }, { - "name": "showcaseMetric", + "type": "widget", "flex": 3, "scrollable": false, - "type": "widget", + "name": "showcaseMetric", "value": "01", "label": "Registered widget", "detail": "This single metric card is the deliberately custom extension point in the content deck.", @@ -3860,7 +3860,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "centerLeft", "flex": 4, "margin": { @@ -3876,12 +3876,22 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/momentum_ribbon.png", "fit": "cover", "scale": 1.24 }, { + "type": "block", + "align": "centerLeft", + "flex": 5, + "padding": { + "top": 0.0, + "right": 54.0, + "bottom": 0.0, + "left": 0.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3942,17 +3952,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 5, - "padding": { - "top": 0.0, - "right": 54.0, - "bottom": 0.0, - "left": 0.0 - }, - "scrollable": false, - "type": "block" + } } ], "align": "center", diff --git a/demo/layout_showcase/slides.md b/demo/layout_showcase/slides.md index 37c34544..fbdc431d 100644 --- a/demo/layout_showcase/slides.md +++ b/demo/layout_showcase/slides.md @@ -489,22 +489,22 @@ Pixels stretch to occupy both axes. margin: { vertical: 16 } } -#### SCALE DOWN +#### FULL BLEED -### Source leads. +### Frame leads. -Margin and padding create a floating study. +Zero margin and padding let the image fill its card. @image { src: layout_showcase/assets/material_detail.png - fit: scaleDown + fit: cover width: 320 height: 205 - scale: 0.82 + scale: 1 flex: 4 - align: bottomRight - padding: 18 - margin: { horizontal: 8, vertical: 16 } + align: center + padding: 0 + margin: 0 } --- From d5b9e9269e9f0a140efc7b3ff9c8d16cd4d70d6c Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 25 Sep 2026 17:40:24 -0400 Subject: [PATCH 6/7] style(demo): polish layout showcase visual system --- demo/layout_showcase/.superdeck/build_status.json | 2 +- demo/layout_showcase/.superdeck/superdeck.json | 8 ++++---- demo/layout_showcase/.superdeck/superdeck_full.json | 8 ++++---- demo/layout_showcase/slides.md | 4 ++-- demo/lib/src/layout_showcase/layout_showcase.dart | 2 +- demo/lib/src/layout_showcase/showcase_parts.dart | 5 +---- demo/lib/src/layout_showcase/showcase_style.dart | 12 ++++++------ 7 files changed, 19 insertions(+), 22 deletions(-) diff --git a/demo/layout_showcase/.superdeck/build_status.json b/demo/layout_showcase/.superdeck/build_status.json index 49bbecf5..fa1e4389 100644 --- a/demo/layout_showcase/.superdeck/build_status.json +++ b/demo/layout_showcase/.superdeck/build_status.json @@ -1,5 +1,5 @@ { "status": "success", - "timestamp": "2026-09-25T20:42:22.110996Z", + "timestamp": "2026-09-25T21:32:53.564719Z", "slideCount": 13 } \ No newline at end of file diff --git a/demo/layout_showcase/.superdeck/superdeck.json b/demo/layout_showcase/.superdeck/superdeck.json index 5c930c2f..629248dd 100644 --- a/demo/layout_showcase/.superdeck/superdeck.json +++ b/demo/layout_showcase/.superdeck/superdeck.json @@ -137,7 +137,7 @@ "comments": [] }, { - "key": "7BuNcOQM", + "key": "LQmwzcJ0", "options": { "title": "Asymmetric composition", "style": "compact" @@ -177,9 +177,9 @@ "align": "centerRight", "flex": 7, "margin": { - "top": 6.0, + "top": 10.0, "right": 0.0, - "bottom": 6.0, + "bottom": 10.0, "left": 0.0 }, "padding": { @@ -192,7 +192,7 @@ "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", - "scale": 1.12 + "scale": 1 } ], "align": "center", diff --git a/demo/layout_showcase/.superdeck/superdeck_full.json b/demo/layout_showcase/.superdeck/superdeck_full.json index c77c4d66..03b2e3eb 100644 --- a/demo/layout_showcase/.superdeck/superdeck_full.json +++ b/demo/layout_showcase/.superdeck/superdeck_full.json @@ -422,7 +422,7 @@ "comments": [] }, { - "key": "7BuNcOQM", + "key": "LQmwzcJ0", "options": { "title": "Asymmetric composition", "style": "compact" @@ -615,9 +615,9 @@ "align": "centerRight", "flex": 7, "margin": { - "top": 6.0, + "top": 10.0, "right": 0.0, - "bottom": 6.0, + "bottom": 10.0, "left": 0.0 }, "padding": { @@ -630,7 +630,7 @@ "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", - "scale": 1.12 + "scale": 1 } ], "align": "center", diff --git a/demo/layout_showcase/slides.md b/demo/layout_showcase/slides.md index fbdc431d..626775a7 100644 --- a/demo/layout_showcase/slides.md +++ b/demo/layout_showcase/slides.md @@ -138,11 +138,11 @@ A strong composition begins with **one dominant relationship**. Here, a 4:7 spli @image { src: layout_showcase/assets/architectural_steps.png fit: cover - scale: 1.12 + scale: 1 flex: 7 align: centerRight padding: 0 - margin: { vertical: 6 } + margin: { vertical: 10 } } @section { diff --git a/demo/lib/src/layout_showcase/layout_showcase.dart b/demo/lib/src/layout_showcase/layout_showcase.dart index e64f808a..9cdd3852 100644 --- a/demo/lib/src/layout_showcase/layout_showcase.dart +++ b/demo/lib/src/layout_showcase/layout_showcase.dart @@ -30,7 +30,7 @@ class LayoutShowcaseApp extends StatelessWidget { const LayoutShowcaseApp({ super.key, this.deckLoader, - this.transitionDuration = const Duration(milliseconds: 650), + this.transitionDuration = const Duration(milliseconds: 500), }); final DeckLoader? deckLoader; diff --git a/demo/lib/src/layout_showcase/showcase_parts.dart b/demo/lib/src/layout_showcase/showcase_parts.dart index 0d8362a5..8b94d4cb 100644 --- a/demo/lib/src/layout_showcase/showcase_parts.dart +++ b/demo/lib/src/layout_showcase/showcase_parts.dart @@ -9,10 +9,7 @@ class ShowcaseBackground extends StatelessWidget { static const _accents = [ Color(0xFFFF7A59), Color(0xFF8B7CFF), - Color(0xFFFFB15A), - Color(0xFFDF6FFF), Color(0xFF58D5C9), - Color(0xFFFF876B), ]; @override @@ -77,7 +74,7 @@ class ShowcaseFooter extends StatelessWidget implements PreferredSizeWidget { const Text( 'SUPERDECK / LAYOUT STUDY', style: TextStyle( - color: Color(0xFF77737D), + color: Color(0xFFA6A1AA), fontSize: 11, fontWeight: FontWeight.w600, letterSpacing: 2.2, diff --git a/demo/lib/src/layout_showcase/showcase_style.dart b/demo/lib/src/layout_showcase/showcase_style.dart index 1f9db006..fca341d8 100644 --- a/demo/lib/src/layout_showcase/showcase_style.dart +++ b/demo/lib/src/layout_showcase/showcase_style.dart @@ -16,6 +16,8 @@ const _mutedInk = Color(0xFFC9C6CE); const _coral = Color(0xFFFF8A65); const _violet = Color(0xFFA890FF); const _teal = Color(0xFF59D6C8); +const _cardRadius = 24.0; +const _cardBorder = Color(0x3DFFFFFF); SlideStyler showcaseBaseStyle() { return SlideStyler( @@ -163,9 +165,9 @@ SlideStyler showcaseBaseStyle() { decoration: BoxDecorationMix( color: const Color(0xFF121219), border: BorderMix.all( - BorderSideMix(color: const Color(0x2EFFFFFF), width: 1), + BorderSideMix(color: _cardBorder, width: 1), ), - borderRadius: BorderRadiusMix.circular(30), + borderRadius: BorderRadiusMix.circular(_cardRadius), ), ), ), @@ -269,7 +271,7 @@ SlideStyler showcasePanelStyle() { blockContainer: BlockStyler( padding: EdgeInsetsGeometryMix.symmetric(horizontal: 24, vertical: 22), margin: EdgeInsetsGeometryMix.symmetric(vertical: 8), - decoration: _panelDecoration(radius: 22), + decoration: _panelDecoration(radius: _cardRadius), ), ), ); @@ -333,9 +335,7 @@ BoxDecorationMix _panelDecoration({required double radius}) { end: Alignment.bottomRight, colors: const [Color(0x14FFFFFF), Color(0x08FFFFFF)], ), - border: BorderMix.all( - BorderSideMix(color: const Color(0x24FFFFFF), width: 1), - ), + border: BorderMix.all(BorderSideMix(color: _cardBorder, width: 1)), borderRadius: BorderRadiusMix.circular(radius), ); } From 69c880a89d7526ddaec7c92737d8f55a598c8cda Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 25 Sep 2026 17:55:14 -0400 Subject: [PATCH 7/7] fix(superdeck): preserve pre-rendered image Hero blending --- docs/guides/markdown-authoring.mdx | 5 ++- docs/reference/deck-options.mdx | 4 +- docs/reference/markdown-syntax.mdx | 2 +- .../ui/widgets/image_hero_flight_widget.dart | 10 +++-- .../src/markdown/hero_transition_test.dart | 13 ++++--- .../src/ui/widgets/hero_element_test.dart | 37 ++++++------------- 6 files changed, 32 insertions(+), 39 deletions(-) diff --git a/docs/guides/markdown-authoring.mdx b/docs/guides/markdown-authoring.mdx index 90b0c314..6ff51f96 100644 --- a/docs/guides/markdown-authoring.mdx +++ b/docs/guides/markdown-authoring.mdx @@ -281,8 +281,9 @@ Add class names to animate elements between slides: Elements with matching class names animate smoothly during slide transitions. -For images, matching sources remain continuous while different sources -cross-fade during the Hero flight. Standalone images can also participate in +For images, matching sources remain continuous when they can share a renderer; +pre-rendered endpoints and different sources cross-fade during the Hero flight. +Standalone images can also participate in automatic image transitions by position; see the [`animateImages`](../reference/deck-options#animateimages) reference for configuration details. diff --git a/docs/reference/deck-options.mdx b/docs/reference/deck-options.mdx index 0719b2e0..b0ad7cf2 100644 --- a/docs/reference/deck-options.mdx +++ b/docs/reference/deck-options.mdx @@ -220,8 +220,8 @@ consecutive slides so they animate through Hero transitions. Images nested in blockquotes, alerts, or list items do not take part. Set it to `false` to turn automatic image transitions off for the whole deck. Explicit Markdown Hero tags, such as `![Diagram](assets/roadmap.png) {.hero-visual}`, are unaffected. When -paired images share a source, the flight keeps one continuous image; different -sources cross-fade between the two endpoints. +paired images share a source, the flight keeps one continuous image when it can; +pre-rendered endpoints and different sources cross-fade. ```dart DeckOptions( diff --git a/docs/reference/markdown-syntax.mdx b/docs/reference/markdown-syntax.mdx index 3bd155a2..1c691c8e 100644 --- a/docs/reference/markdown-syntax.mdx +++ b/docs/reference/markdown-syntax.mdx @@ -227,6 +227,6 @@ Use Markdown class markers to animate matching elements between slides. The firs Use the same tag for the element that should transition from one slide to the next. For example, one `{.heading}` on slide A can animate to one `{.heading}` on slide B. -Standalone Markdown images and `@image` blocks transition automatically by image order: the first image on one slide moves to the first on the next slide, the second to the second, and so on. Images with the same source keep one continuous image during the flight; different sources cross-fade between the two endpoints. Use an explicit Markdown class marker when an image needs to match by a named identity instead of its position. Images without a partner on the adjacent slide render normally. Images nested in blockquotes, alerts, or list items do not take part, and `DeckOptions(animateImages: false)` turns automatic image transitions off for the whole deck. +Standalone Markdown images and `@image` blocks transition automatically by image order: the first image on one slide moves to the first on the next slide, the second to the second, and so on. Images with the same source keep one continuous image when they can share a renderer; pre-rendered endpoints and different sources cross-fade. Use an explicit Markdown class marker when an image needs to match by a named identity instead of its position. Images without a partner on the adjacent slide render normally. Images nested in blockquotes, alerts, or list items do not take part, and `DeckOptions(animateImages: false)` turns automatic image transitions off for the whole deck. Do not use the same tag twice on one slide. Each Hero tag must identify one element per slide, so use separate tags like `{.heading}` and `{.subheading}` when multiple elements should animate. diff --git a/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart b/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart index 8e1f00ef..33b2eefa 100644 --- a/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart +++ b/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart @@ -54,12 +54,14 @@ class ImageHeroFlight extends StatelessWidget { // Hero's overlay already interpolates the measured endpoint rectangles. // A second size tween from block dimensions is wrong for intrinsically sized // images and images with only one explicit dimension. - final sameUri = from.uri == to.uri; - final endpoint = to.flightImage ?? from.flightImage; + final canReuseSource = + from.uri == to.uri && + from.flightImage == null && + to.flightImage == null; return SizedBox.expand( key: const ValueKey('image-hero-flight'), - child: sameUri - ? endpoint ?? _image(from, spec) + child: canReuseSource + ? _image(from, spec) : Stack( fit: StackFit.expand, children: [ diff --git a/packages/superdeck/test/src/markdown/hero_transition_test.dart b/packages/superdeck/test/src/markdown/hero_transition_test.dart index 055096fc..6c74dd0b 100644 --- a/packages/superdeck/test/src/markdown/hero_transition_test.dart +++ b/packages/superdeck/test/src/markdown/hero_transition_test.dart @@ -553,12 +553,15 @@ void main() { tester.getSize(find.byKey(const ValueKey('image-hero-flight'))), const Size(180, 120), ); - expect( - tester - .widget(find.byKey(const ValueKey('image-hero-flight'))) - .child, - same(to.flightImage), + final flight = tester.widget( + find.byKey(const ValueKey('image-hero-flight')), ); + final blend = flight.child! as Stack; + expect(blend.children, hasLength(2)); + expect((blend.children[0] as Opacity).child, same(from.flightImage)); + expect((blend.children[0] as Opacity).opacity, 0.5); + expect((blend.children[1] as Opacity).child, same(to.flightImage)); + expect((blend.children[1] as Opacity).opacity, 0.5); expect(tester.takeException(), isNull); }); diff --git a/packages/superdeck/test/src/ui/widgets/hero_element_test.dart b/packages/superdeck/test/src/ui/widgets/hero_element_test.dart index 000cf3a2..56f9a0c2 100644 --- a/packages/superdeck/test/src/ui/widgets/hero_element_test.dart +++ b/packages/superdeck/test/src/ui/widgets/hero_element_test.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:superdeck/src/ui/widgets/hero_element.dart'; +import 'package:superdeck/src/ui/widgets/text_hero_flight.dart'; /// Regression coverage for the "text size pops at the start/end of a hero /// transition" bug. @@ -19,27 +20,11 @@ void main() { ); const text = 'Heading Sample'; const routeKey = ValueKey('route-text'); - const shuttleKey = ValueKey('shuttle-text'); - - Widget heroWidget() => HeroElement( - data: text, - child: buildElementHero( - tag: 'h', - // Mirrors StyledText -> Text(text, style: spec.style). - child: const Text(text, style: headingStyle, key: routeKey), - flightShuttleBuilder: - (context, animation, direction, fromContext, toContext) { - // Mirrors TextElementBuilder._buildStableFlight at a flight endpoint. - return Text.rich( - TextSpan( - style: headingStyle, - children: [TextSpan(text: text)], - ), - style: DefaultTextStyle.of(fromContext).style, - key: shuttleKey, - ); - }, - ), + Widget heroWidget() => buildElementHero( + tag: 'h', + // Mirrors StyledText -> Text(text, style: spec.style). + child: const Text(text, style: headingStyle, key: routeKey), + flightShuttleBuilder: buildTextHeroFlight, ); testWidgets( @@ -66,10 +51,12 @@ void main() { await tester.pump(const Duration(milliseconds: 1)); // first flight frame final shuttlePara = tester.renderObject( - find.descendant( - of: find.byKey(shuttleKey), - matching: find.byType(RichText), - ), + find + .descendant( + of: find.byType(FittedBox), + matching: find.byType(RichText), + ) + .first, ); final shuttleWidth = shuttlePara.getMaxIntrinsicWidth(double.infinity); final shuttleHeight = shuttlePara.getMinIntrinsicHeight(double.infinity);