diff --git a/doc/marketing_goldens/flutter_test_config.dart b/doc/marketing_goldens/flutter_test_config.dart new file mode 100644 index 0000000..2b7e9cd --- /dev/null +++ b/doc/marketing_goldens/flutter_test_config.dart @@ -0,0 +1,17 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter_test_goldens/flutter_test_goldens.dart'; +import 'package:super_text_layout/super_text_layout.dart'; + +Future testExecutable(FutureOr Function() testMain) async { + // Adjust the theme that's applied to all golden tests in this suite. + GoldenSceneTheme.push(GoldenSceneTheme.standard.copyWith( + directory: Directory("."), + )); + + // Disable animations in Super Editor. + BlinkController.indeterminateAnimationsEnabled = false; + + return testMain(); +} diff --git a/doc/marketing_goldens/gallery/gallery.png b/doc/marketing_goldens/gallery/gallery.png new file mode 100644 index 0000000..00c851b Binary files /dev/null and b/doc/marketing_goldens/gallery/gallery.png differ diff --git a/doc/marketing_goldens/gallery/gallery_test.dart b/doc/marketing_goldens/gallery/gallery_test.dart new file mode 100644 index 0000000..e4ea5c8 --- /dev/null +++ b/doc/marketing_goldens/gallery/gallery_test.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test_goldens/flutter_test_goldens.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../shadcn_test_tools.dart'; + +void main() { + group("Marketing >", () { + testGoldenScene("gallery", (tester) async { + FtgLog.initAllLogs(); + + // The following code is broken up so it can spread across multiple slides. + final gallery = Gallery( + "Gallery", + fileName: "gallery", + itemScaffold: shadcnItemScaffold, + layout: ShadcnGalleryLayout( + shadcnWordmarkProvider: shadcnWordmarkProvider, + ), + ); + + gallery + .itemFromWidget( + id: "1", + description: "Primary", + widget: ShadButton( + child: const Text('Primary'), + onPressed: () {}, + ), + ) + .itemFromWidget( + id: "2", + description: "Secondary", + widget: ShadButton.secondary( + child: const Text('Secondary'), + onPressed: () {}, + ), + ) + .itemFromWidget( + id: "3", + description: "Destructive", + widget: ShadButton.destructive( + child: const Text('Destructive'), + onPressed: () {}, + ), + ) + .itemFromWidget( + id: "4", + description: "Ghost", + widget: ShadButton.ghost( + child: const Text('Ghost'), + onPressed: () {}, + ), + ) + .itemFromWidget( + id: "5", + description: "Link", + widget: ShadButton.ghost( + child: const Text('Link'), + onPressed: () {}, + ), + ) + .itemFromWidget( + id: "6", + description: "Icon + Label", + widget: ShadButton( + onPressed: () {}, + leading: const Icon(LucideIcons.mail), + child: const Text('Login with Email'), + ), + ) + .itemFromBuilder( + id: "7", + description: "Loading", + builder: (context) { + return ShadButton( + onPressed: () {}, + leading: SizedBox.square( + dimension: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: ShadTheme.of(context).colorScheme.primaryForeground, + ), + ), + child: const Text('Please wait'), + ); + }, + ) + .itemFromWidget( + id: "8", + description: "Gradient + Shadow", + widget: ShadButton( + onPressed: () {}, + gradient: const LinearGradient(colors: [ + Colors.cyan, + Colors.indigo, + ]), + shadows: [ + BoxShadow( + color: Colors.blue.withOpacity(.4), + spreadRadius: 4, + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + child: const Text('Gradient with Shadow'), + ), + ); + + await gallery.run(tester); + }); + }); +} diff --git a/doc/marketing_goldens/magazine/slack_chat_magazine_layout.png b/doc/marketing_goldens/magazine/slack_chat_magazine_layout.png new file mode 100644 index 0000000..c3795ac Binary files /dev/null and b/doc/marketing_goldens/magazine/slack_chat_magazine_layout.png differ diff --git a/doc/marketing_goldens/marketing_ui_toolkit.dart b/doc/marketing_goldens/marketing_ui_toolkit.dart new file mode 100644 index 0000000..dbd6c72 --- /dev/null +++ b/doc/marketing_goldens/marketing_ui_toolkit.dart @@ -0,0 +1,65 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class AngledLinePainter extends CustomPainter { + AngledLinePainter({ + required this.angleDegrees, + required this.gap, + required this.thickness, + required this.lineColor, + this.backgroundColor = Colors.transparent, + }); + + final double angleDegrees; + final double gap; + final double thickness; + final Color lineColor; + final Color backgroundColor; + + @override + void paint(Canvas canvas, Size size) { + final rect = Offset.zero & size; + canvas + ..save() + ..clipRect(rect); + + // Fill background. + canvas.drawRect(rect, Paint()..color = backgroundColor); + + // Draw lines. + final paint = Paint() + ..color = lineColor + ..strokeWidth = thickness; + + final angleRadians = angleDegrees * pi / 180; + + // Calculate line direction + final dx = cos(angleRadians); + final dy = sin(angleRadians); + final direction = Offset(dx, dy); + final perpendicular = Offset(-dy, dx); // unit perpendicular vector + + // Calculate diagonal length to cover the canvas + final diagonal = sqrt(size.width * size.width + size.height * size.height); + + // Center of canvas + final center = Offset(size.width / 2, size.height / 2); + + // Number of lines needed to cover the canvas + final numLines = (diagonal / gap).ceil(); + + for (int i = -numLines; i <= numLines; i++) { + final offset = perpendicular * (i * gap); + final start = center + offset - direction * diagonal; + final end = center + offset + direction * diagonal; + + canvas.drawLine(start, end, paint); + } + + canvas.restore(); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} diff --git a/doc/marketing_goldens/platform_adaptive_gallery/icons/android-brands.svg b/doc/marketing_goldens/platform_adaptive_gallery/icons/android-brands.svg new file mode 100644 index 0000000..950d18b --- /dev/null +++ b/doc/marketing_goldens/platform_adaptive_gallery/icons/android-brands.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/marketing_goldens/platform_adaptive_gallery/icons/apple-brands.svg b/doc/marketing_goldens/platform_adaptive_gallery/icons/apple-brands.svg new file mode 100644 index 0000000..a9fcef8 --- /dev/null +++ b/doc/marketing_goldens/platform_adaptive_gallery/icons/apple-brands.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/marketing_goldens/platform_adaptive_gallery/icons/laptop-solid.svg b/doc/marketing_goldens/platform_adaptive_gallery/icons/laptop-solid.svg new file mode 100644 index 0000000..eeb9595 --- /dev/null +++ b/doc/marketing_goldens/platform_adaptive_gallery/icons/laptop-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/marketing_goldens/platform_adaptive_gallery/icons/ubuntu-brands.svg b/doc/marketing_goldens/platform_adaptive_gallery/icons/ubuntu-brands.svg new file mode 100644 index 0000000..38e73a4 --- /dev/null +++ b/doc/marketing_goldens/platform_adaptive_gallery/icons/ubuntu-brands.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/marketing_goldens/platform_adaptive_gallery/icons/windows-brands.svg b/doc/marketing_goldens/platform_adaptive_gallery/icons/windows-brands.svg new file mode 100644 index 0000000..492d749 --- /dev/null +++ b/doc/marketing_goldens/platform_adaptive_gallery/icons/windows-brands.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/marketing_goldens/platform_adaptive_gallery/platform_adaptive_gallery.png b/doc/marketing_goldens/platform_adaptive_gallery/platform_adaptive_gallery.png new file mode 100644 index 0000000..d9caaab Binary files /dev/null and b/doc/marketing_goldens/platform_adaptive_gallery/platform_adaptive_gallery.png differ diff --git a/doc/marketing_goldens/platform_adaptive_gallery/platform_adaptive_gallery_test.dart b/doc/marketing_goldens/platform_adaptive_gallery/platform_adaptive_gallery_test.dart new file mode 100644 index 0000000..73df3fd --- /dev/null +++ b/doc/marketing_goldens/platform_adaptive_gallery/platform_adaptive_gallery_test.dart @@ -0,0 +1,132 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test_goldens/flutter_test_goldens.dart'; +import 'package:flutter_test_goldens/golden_bricks.dart'; +import 'package:super_editor/super_editor.dart'; +import 'package:super_editor/super_editor_test.dart'; + +void main() { + group("Marketing > gallery > platform adaptive >", () { + testGoldenScene("text editor", (tester) async { + FtgLog.initLoggers({FtgLog.pipeline}); + + await Gallery( + "Platform Adaptive Gallery", + fileName: "platform_adaptive_gallery", + layout: GridGoldenSceneLayout( + itemDecorator: _itemDecorator, + ), + ) + .itemFromPumper( + id: "selected-text", + description: "Selected Text", + forEachPlatform: true, + pumper: _pumpEditor, + ) + .run(tester); + }); + }); +} + +Future _pumpEditor( + WidgetTester tester, + GoldenSceneItemScaffold scaffold, + String description, +) async { + final editor = _createEditor(); + + await tester.pumpWidget( + scaffold( + tester, + DefaultTextStyle( + style: TextStyle(fontFamily: goldenBricks), + child: SizedBox( + width: 600, + child: SuperEditor( + editor: editor, + stylesheet: defaultStylesheet.copyWith(inlineTextStyler: (attributions, style) { + style = defaultInlineTextStyler(attributions, style); + return style.copyWith(fontFamily: goldenBricks); + }), + shrinkWrap: true, + ), + ), + ), + ), + ); + + await tester.doubleTapInParagraph("1", 25); +} + +Editor _createEditor() { + return createDefaultDocumentEditor( + document: _createDocument(), + composer: MutableDocumentComposer(), + ); +} + +MutableDocument _createDocument() { + return MutableDocument( + nodes: [ + ParagraphNode( + id: "0", + text: AttributedText("Multi Platform!"), + metadata: { + NodeMetadata.blockType: header1Attribution, + }, + ), + ParagraphNode( + id: "1", + text: AttributedText("Hello, world! This is a document editor built with Flutter."), + ), + ParagraphNode( + id: "2", + text: AttributedText("You can place the caret, type text, delete text, select text, etc."), + ), + ], + ); +} + +Widget _itemDecorator( + BuildContext context, + GoldenScreenshotMetadata metadata, + Widget content, +) { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + content, + Divider(), + Row( + mainAxisSize: MainAxisSize.min, + spacing: 16, + children: [ + SvgPicture.file( + _getIconFileForPlatform(metadata.simulatedPlatform), + color: Colors.black, + width: 24, + height: 24, + ), + Text(metadata.description), + ], + ), + ], + ), + ); +} + +File _getIconFileForPlatform(TargetPlatform platform) => switch (platform) { + TargetPlatform.android => File("doc/marketing_goldens/platform_adaptive_gallery/icons/android-brands.svg"), + TargetPlatform.iOS => File("doc/marketing_goldens/platform_adaptive_gallery/icons/apple-brands.svg"), + TargetPlatform.macOS => File("doc/marketing_goldens/platform_adaptive_gallery/icons/laptop-solid.svg"), + TargetPlatform.windows => File("doc/marketing_goldens/platform_adaptive_gallery/icons/windows-brands.svg"), + TargetPlatform.linux => File("doc/marketing_goldens/platform_adaptive_gallery/icons/ubuntu-brands.svg"), + TargetPlatform.fuchsia => throw UnimplementedError(), + }; diff --git a/doc/marketing_goldens/shadcn_test_tools.dart b/doc/marketing_goldens/shadcn_test_tools.dart new file mode 100644 index 0000000..e0d590f --- /dev/null +++ b/doc/marketing_goldens/shadcn_test_tools.dart @@ -0,0 +1,269 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test_goldens/flutter_test_goldens.dart'; +import 'package:golden_bricks/golden_bricks.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +final shadcnWordmark = File("doc/marketing_goldens/single_shot/shadcn_wordmark.png").readAsBytesSync(); +final shadcnWordmarkProvider = MemoryImage(shadcnWordmark); + +/// An item scaffold that builds a Shadcn scaffold. +Widget shadcnItemScaffold(WidgetTester tester, Widget content) { + return buildShadcnScaffold(content); +} + +/// Builds a widget tree with Shadcn dependencies at the root, and [content] within +/// its subtree. +Widget buildShadcnScaffold(Widget content) { + return ShadApp( + theme: ShadThemeData( + brightness: Brightness.dark, + colorScheme: ShadBlueColorScheme.dark(), + textTheme: ShadTextTheme( + family: goldenBricks, + ), + ), + home: Scaffold( + body: Builder(builder: (context) { + return DefaultTextStyle( + style: DefaultTextStyle.of(context).style.copyWith(fontFamily: goldenBricks), + child: GoldenImageBounds( + child: content, + ), + ); + }), + ), + debugShowCheckedModeBanner: false, + ); +} + +class ShadcnSingleShotSceneLayout implements SceneLayout { + const ShadcnSingleShotSceneLayout({ + required this.shadcnWordmarkProvider, + this.aspectRatio, + }); + + final ImageProvider shadcnWordmarkProvider; + + /// The desired aspect ratio of the final golden, which is useful when creating + /// website header version. + /// + /// When `null`, no aspect ratio is enforced. + final double? aspectRatio; + + @override + Widget build( + WidgetTester tester, + BuildContext context, + Map>> goldens, + ) { + final golden = goldens.entries.first; + + return DefaultTextStyle( + style: GoldenSceneTheme.current.defaultTextStyle.copyWith( + color: ShadBlueColorScheme.dark().primary, + ), + child: IntrinsicWidth( + child: IntrinsicHeight( + child: GoldenSceneBounds( + child: aspectRatio != null + ? AspectRatio( + aspectRatio: aspectRatio!, + child: _buildContent(golden), + ) + : _buildContent(golden), + ), + ), + ), + ); + } + + Widget _buildContent(MapEntry golden) { + return ShadcnBackground( + child: Center( + child: Container( + margin: const EdgeInsets.all(48), + padding: const EdgeInsets.all(48), + color: ShadBlueColorScheme.dark().background, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 24, + children: [ + Image( + image: shadcnWordmarkProvider, + height: 30, + fit: BoxFit.contain, + ), + Image.memory( + key: golden.value, + golden.key.pngBytes, + width: golden.key.size.width.toDouble(), + height: golden.key.size.height.toDouble(), + ), + Text( + "Calendar - Current Month", + style: TextStyle( + color: ShadBlueColorScheme.dark().primary, + fontSize: 18, + ), + ), + ], + ), + ), + ), + ); + } +} + +class ShadcnGalleryLayout implements SceneLayout { + const ShadcnGalleryLayout({ + required this.shadcnWordmarkProvider, + }); + + final ImageProvider shadcnWordmarkProvider; + + @override + Widget build( + WidgetTester tester, + BuildContext context, + Map>> goldens, + ) { + final entries = goldens.entries.toList(); + + return DefaultTextStyle( + style: GoldenSceneTheme.current.defaultTextStyle.copyWith( + color: ShadBlueColorScheme.dark().primary, + ), + child: GoldenSceneBounds( + child: CustomPaint( + painter: AngledLinePainter( + angleDegrees: -45, + gap: 50, + thickness: 5, + lineColor: ShadBlueColorScheme.dark().accent.withValues(alpha: 0.2), + backgroundColor: ShadBlueColorScheme.dark().background, + ), + child: Padding( + padding: const EdgeInsets.all(48), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 24, + children: [ + Image( + image: shadcnWordmarkProvider, + height: 30, + fit: BoxFit.contain, + ), + Table( + defaultColumnWidth: IntrinsicColumnWidth(), + children: _buildRows(entries), + ), + ], + ), + ), + ), + ), + ); + } + + List _buildRows(List> entries) { + final rows = []; + for (int row = 0; row < entries.length / 3; row += 1) { + final items = []; + for (int col = 0; col < 3; col += 1) { + final index = row * 3 + col; + if (index >= entries.length) { + items.add(const SizedBox()); + continue; + } + + items.add( + Padding( + padding: const EdgeInsets.all(24), + child: Container( + padding: const EdgeInsets.all(48), + color: ShadBlueColorScheme.dark().background, + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 24, + children: [ + Image.memory( + key: entries[index].value, + entries[index].key.pngBytes, + width: entries[index].key.size.width, + height: entries[index].key.size.height, + ), + Text( + entries[index].key.metadata.description, + style: TextStyle( + fontSize: 14, + ), + ), + ], + ), + ), + ), + ); + } + + rows.add( + TableRow( + children: items, + ), + ); + } + + return rows; + } +} + +class ShadcnBackground extends StatelessWidget { + const ShadcnBackground({ + super.key, + this.child, + }); + + final Widget? child; + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: AngledLinePainter( + angleDegrees: -45, + gap: 50, + thickness: 5, + lineColor: ShadBlueColorScheme.dark().accent.withValues(alpha: 0.2), + backgroundColor: ShadBlueColorScheme.dark().background, + ), + child: child, + ); + } +} + +Widget shadcnItemDecorator( + BuildContext context, + GoldenScreenshotMetadata metadata, + Widget content, +) { + return ColoredBox( + color: ShadBlueColorScheme.dark().background, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + content, + Padding( + padding: const EdgeInsets.all(24), + child: Text( + metadata.description, + style: TextStyle(fontFamily: TestFonts.openSans), + ), + ), + ], + ), + ); +} diff --git a/doc/marketing_goldens/single_shot/shadcn_wordmark.png b/doc/marketing_goldens/single_shot/shadcn_wordmark.png new file mode 100644 index 0000000..054a4f0 Binary files /dev/null and b/doc/marketing_goldens/single_shot/shadcn_wordmark.png differ diff --git a/doc/marketing_goldens/single_shot/single_shot_standalone.png b/doc/marketing_goldens/single_shot/single_shot_standalone.png new file mode 100644 index 0000000..b9d5a1d Binary files /dev/null and b/doc/marketing_goldens/single_shot/single_shot_standalone.png differ diff --git a/doc/marketing_goldens/single_shot/single_shot_test.dart b/doc/marketing_goldens/single_shot/single_shot_test.dart new file mode 100644 index 0000000..59557ec --- /dev/null +++ b/doc/marketing_goldens/single_shot/single_shot_test.dart @@ -0,0 +1,36 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test_goldens/flutter_test_goldens.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../shadcn_test_tools.dart'; + +Future main() async { + group("Marketing >", () { + testGoldenScene("single shot", (tester) async { + await tester.runAsync(() async { + await precacheImage(shadcnWordmarkProvider, tester.binding.rootElement!); + }); + + await SingleShot("Standalone", fileName: "single_shot_standalone") // + .fromWidget(ShadCalendar()) + .inScaffold(shadcnItemScaffold) + .withLayout(ShadcnSingleShotSceneLayout( + shadcnWordmarkProvider: shadcnWordmarkProvider, + )) + .run(tester); + + await SingleShot("Header", fileName: "single_shot_website_header") // + .fromWidget(ShadCalendar()) + .inScaffold(shadcnItemScaffold) + .withLayout(ShadcnSingleShotSceneLayout( + shadcnWordmarkProvider: shadcnWordmarkProvider, + aspectRatio: 3 / 1, + )) + .run(tester); + }); + }); +} diff --git a/doc/marketing_goldens/single_shot/single_shot_website_header.png b/doc/marketing_goldens/single_shot/single_shot_website_header.png new file mode 100644 index 0000000..3d952a5 Binary files /dev/null and b/doc/marketing_goldens/single_shot/single_shot_website_header.png differ diff --git a/doc/marketing_goldens/timeline_animation/animation_test.dart b/doc/marketing_goldens/timeline_animation/animation_test.dart new file mode 100644 index 0000000..5d55fb8 --- /dev/null +++ b/doc/marketing_goldens/timeline_animation/animation_test.dart @@ -0,0 +1,110 @@ +import 'package:animated_toggle_switch/animated_toggle_switch.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test_goldens/flutter_test_goldens.dart'; + +void main() { + group("Marketing > timeline >", () { + testGoldenScene("animation", (tester) async { + await Timeline( + "Crazy Switch - Flip animation", + fileName: 'crazy-switch_5-shot', + layout: AnimationTimelineSceneLayout(), + ) + .setupWithWidget(Padding( + padding: const EdgeInsets.all(48), + child: CrazySwitch(), + )) + .takePhoto("Off") + .tap(find.byType(CrazySwitch)) + .takePhotos(3, const Duration(milliseconds: 100)) + .settle() + .takePhoto("On") + .run(tester); + + await Timeline( + "Crazy Switch - Flip animation", + fileName: 'crazy-switch_8-shot', + layout: AnimationTimelineSceneLayout(), + ) + .setupWithWidget(Padding( + padding: const EdgeInsets.all(48), + child: CrazySwitch(), + )) + .takePhoto("Off") + .tap(find.byType(CrazySwitch)) + .takePhotos(6, const Duration(milliseconds: 50)) + .settle() + .takePhoto("On") + .run(tester); + }); + }); +} + +class CrazySwitch extends StatefulWidget { + const CrazySwitch({super.key}); + + @override + State createState() => _CrazySwitchState(); +} + +class _CrazySwitchState extends State { + bool current = false; + + @override + Widget build(BuildContext context) { + const red = Color(0xFFFD0821); + const green = Color(0xFF46E82E); + const borderWidth = 10.0; + const height = 58.0; + const innerIndicatorSize = height - 4 * borderWidth; + + return CustomAnimatedToggleSwitch( + current: current, + spacing: 36.0, + values: const [false, true], + animationDuration: const Duration(milliseconds: 350), + animationCurve: Curves.bounceOut, + iconBuilder: (context, local, global) => const SizedBox(), + onTap: (_) => setState(() => current = !current), + iconsTappable: false, + onChanged: (b) => setState(() => current = b), + height: height, + padding: const EdgeInsets.all(borderWidth), + indicatorSize: const Size.square(height - 2 * borderWidth), + foregroundIndicatorBuilder: (context, global) { + final color = Color.lerp(red, green, global.position)!; + // You can replace the Containers with DecoratedBox/SizedBox/Center + // for slightly better performance + return Container( + alignment: Alignment.center, + decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), + child: Container( + width: innerIndicatorSize * 0.4 + global.position * innerIndicatorSize * 0.6, + height: innerIndicatorSize, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.0), + color: color, + )), + ); + }, + wrapperBuilder: (context, global, child) { + final color = Color.lerp(red, green, global.position)!; + return DecoratedBox( + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(50.0), + boxShadow: [ + BoxShadow( + color: color.withValues(alpha: 0.7), + blurRadius: 12.0, + offset: const Offset(0.0, 8.0), + ), + ], + ), + child: child, + ); + }, + ); + } +} diff --git a/doc/marketing_goldens/timeline_animation/crazy-switch_5-shot.png b/doc/marketing_goldens/timeline_animation/crazy-switch_5-shot.png new file mode 100644 index 0000000..0984ff0 Binary files /dev/null and b/doc/marketing_goldens/timeline_animation/crazy-switch_5-shot.png differ diff --git a/doc/marketing_goldens/timeline_animation/crazy-switch_8-shot.png b/doc/marketing_goldens/timeline_animation/crazy-switch_8-shot.png new file mode 100644 index 0000000..21023d1 Binary files /dev/null and b/doc/marketing_goldens/timeline_animation/crazy-switch_8-shot.png differ diff --git a/doc/marketing_goldens/timeline_interaction/otp.png b/doc/marketing_goldens/timeline_interaction/otp.png new file mode 100644 index 0000000..1e2eb71 Binary files /dev/null and b/doc/marketing_goldens/timeline_interaction/otp.png differ diff --git a/doc/marketing_goldens/timeline_interaction/otp_interaction_test.dart b/doc/marketing_goldens/timeline_interaction/otp_interaction_test.dart new file mode 100644 index 0000000..fa3ba6b --- /dev/null +++ b/doc/marketing_goldens/timeline_interaction/otp_interaction_test.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test_goldens/flutter_test_goldens.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../shadcn_test_tools.dart'; + +void main() { + group("Marketing > timeline > interaction", () { + testGoldenScene("OTP", (tester) async { + FtgLog.initAllLogs(); + + GoldenSceneTheme.useForTest(GoldenSceneTheme.current.copyWith( + defaultTextStyle: TextStyle( + color: ShadBlueColorScheme.dark().primary, + ), + )); + + // The following code is broken apart so we can include pieces across slides. + final timeline = Timeline( + "OTP - Digit Entry", + fileName: 'otp', + itemScaffold: shadcnItemScaffold, + layout: ColumnSceneLayout( + background: GoldenSceneBackground.widget(ShadcnBackground()), + itemDecorator: shadcnItemDecorator, + ), + ); + + timeline.setupWithWidget(Padding( + padding: const EdgeInsets.all(48), + child: ShadInputOTP( + onChanged: (v) {}, + maxLength: 6, + children: [ + const ShadInputOTPGroup( + children: [ + ShadInputOTPSlot(key: ValueKey("1")), + ShadInputOTPSlot(key: ValueKey("2")), + ShadInputOTPSlot(key: ValueKey("3")), + ], + ), + Icon(size: 24, LucideIcons.dot), + const ShadInputOTPGroup( + children: [ + ShadInputOTPSlot(key: ValueKey("4")), + ShadInputOTPSlot(key: ValueKey("5")), + ShadInputOTPSlot(key: ValueKey("6")), + ], + ), + ], + ), + )); + + await timeline + .takePhoto("Idle") + .tap(find.byType(ShadInputOTPSlot).first) + .settle() + .takePhoto("Initial focus") + .modifyScene(_insertOtpAt("1", "a")) + .takePhoto("Type first character") + .modifyScene(_insertOtpAt("2", "b")) + .takePhoto("Type 2nd character") + .modifyScene(_insertOtpAt("3", "c")) + .takePhoto("Type 3rd character") + .modifyScene(_insertOtpAt("4", "1")) + .takePhoto("Type 4th character") + .modifyScene(_insertOtpAt("5", "2")) + .takePhoto("Type 5th character") + .modifyScene(_insertOtpAt("6", "3")) + .takePhoto("Type last character") + .run(tester); + }); + }); +} + +TimelineModifySceneDelegate _insertOtpAt(String key, String character) { + return (tester, testContext) async { + await tester.enterText(find.byKey(ValueKey(key)), character); + await tester.pumpAndSettle(); + }; +} diff --git a/doc/website/source/_includes/components/navMain.jinja b/doc/website/source/_includes/components/navMain.jinja index 127f67e..5b8825a 100644 --- a/doc/website/source/_includes/components/navMain.jinja +++ b/doc/website/source/_includes/components/navMain.jinja @@ -1,21 +1,4 @@