From b340e2ce73489ba877e41f6c12f93984e63669c7 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:48:56 -0300 Subject: [PATCH 01/28] fix: add Semantics wrappers to text and outlined button builders - Wrap buildServerTextButton with Semantics(button: true, label: ...) - Wrap buildServerOutlinedButton with Semantics(button: true, label: ...) Made-with: Cursor --- .../widgets/server_button_variants.dart | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/lib/presentation/widgets/server_button_variants.dart b/lib/presentation/widgets/server_button_variants.dart index 053eccb..c038e0e 100644 --- a/lib/presentation/widgets/server_button_variants.dart +++ b/lib/presentation/widgets/server_button_variants.dart @@ -16,13 +16,17 @@ Widget buildServerTextButton( final fontSize = (node.props['fontSize'] as num?)?.toDouble(); final padding = parsePadding(node.props['padding']); - return TextButton( - onPressed: node.action != null ? () => handleAction(context, node.action) : null, - style: TextButton.styleFrom( - foregroundColor: textColor, - padding: padding, + return Semantics( + button: true, + label: label, + child: TextButton( + onPressed: node.action != null ? () => handleAction(context, node.action) : null, + style: TextButton.styleFrom( + foregroundColor: textColor, + padding: padding, + ), + child: Text(label, style: TextStyle(fontSize: fontSize)), ), - child: Text(label, style: TextStyle(fontSize: fontSize)), ); } @@ -37,15 +41,19 @@ Widget buildServerOutlinedButton( final borderRadius = (node.props['borderRadius'] as num?)?.toDouble() ?? 8; final padding = parsePadding(node.props['padding']); - return OutlinedButton( - onPressed: node.action != null ? () => handleAction(context, node.action) : null, - style: OutlinedButton.styleFrom( - foregroundColor: textColor, - side: borderColor != null ? BorderSide(color: borderColor) : null, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(borderRadius)), - padding: padding, + return Semantics( + button: true, + label: label, + child: OutlinedButton( + onPressed: node.action != null ? () => handleAction(context, node.action) : null, + style: OutlinedButton.styleFrom( + foregroundColor: textColor, + side: borderColor != null ? BorderSide(color: borderColor) : null, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(borderRadius)), + padding: padding, + ), + child: Text(label), ), - child: Text(label), ); } From 0fbc14e81916df5b7da431eb4bdf411856f13372 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:49:05 -0300 Subject: [PATCH 02/28] fix: add Semantics wrappers to inkWell and gestureDetector builders - Wrap buildServerInkWell with Semantics(button: node.action != null) - Wrap buildServerGestureDetector with Semantics for accessibility Made-with: Cursor --- .../widgets/server_interactives.dart | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/presentation/widgets/server_interactives.dart b/lib/presentation/widgets/server_interactives.dart index 55f3f43..846cbff 100644 --- a/lib/presentation/widgets/server_interactives.dart +++ b/lib/presentation/widgets/server_interactives.dart @@ -14,12 +14,15 @@ Widget buildServerInkWell( final splashColor = parseHexColor(node.props['splashColor'] as String?); final highlightColor = parseHexColor(node.props['highlightColor'] as String?); - return InkWell( - onTap: node.action != null ? () => handleAction(context, node.action) : null, - borderRadius: borderRadius, - splashColor: splashColor, - highlightColor: highlightColor, - child: buildSingleChild(node, buildChild), + return Semantics( + button: node.action != null, + child: InkWell( + onTap: node.action != null ? () => handleAction(context, node.action) : null, + borderRadius: borderRadius, + splashColor: splashColor, + highlightColor: highlightColor, + child: buildSingleChild(node, buildChild), + ), ); } @@ -28,10 +31,13 @@ Widget buildServerGestureDetector( BuildContext context, Widget Function(ComponentNode) buildChild, ) { - return GestureDetector( - onTap: node.action != null ? () => handleAction(context, node.action) : null, - behavior: _parseHitTestBehavior(node.props['behavior'] as String?), - child: buildSingleChild(node, buildChild), + return Semantics( + button: node.action != null, + child: GestureDetector( + onTap: node.action != null ? () => handleAction(context, node.action) : null, + behavior: _parseHitTestBehavior(node.props['behavior'] as String?), + child: buildSingleChild(node, buildChild), + ), ); } From 3e6e57a6238fdbf000846b6eadcef780c3109abe Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:49:11 -0300 Subject: [PATCH 03/28] fix: add Semantics wrappers to selectableText and richText builders - Wrap selectableText with Semantics(label: content) - Wrap richText with Semantics using concatenated span text Made-with: Cursor --- .../widgets/server_text_variants.dart | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/lib/presentation/widgets/server_text_variants.dart b/lib/presentation/widgets/server_text_variants.dart index c2fcb5d..962b36d 100644 --- a/lib/presentation/widgets/server_text_variants.dart +++ b/lib/presentation/widgets/server_text_variants.dart @@ -16,14 +16,17 @@ Widget buildServerSelectableText( final textAlign = parseTextAlign(node.props['textAlign'] as String?); final maxLines = (node.props['maxLines'] as num?)?.toInt(); - return SelectableText( - content, - textAlign: textAlign, - maxLines: maxLines, - style: TextStyle( - fontSize: fontSize, - fontWeight: fontWeight, - color: color, + return Semantics( + label: content, + child: SelectableText( + content, + textAlign: textAlign, + maxLines: maxLines, + style: TextStyle( + fontSize: fontSize, + fontWeight: fontWeight, + color: color, + ), ), ); } @@ -56,13 +59,18 @@ Widget buildServerRichText( final maxLines = (node.props['maxLines'] as num?)?.toInt(); final overflow = parseTextOverflow(node.props['overflow'] as String?); - return RichText( - textAlign: textAlign ?? TextAlign.start, - maxLines: maxLines, - overflow: overflow ?? TextOverflow.clip, - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: spans, + final semanticLabel = spans.map((s) => s.text ?? '').join(); + + return Semantics( + label: semanticLabel, + child: RichText( + textAlign: textAlign ?? TextAlign.start, + maxLines: maxLines, + overflow: overflow ?? TextOverflow.clip, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: spans, + ), ), ); } From eff384ec2827577347a6800023494e2b43023cfb Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:49:16 -0300 Subject: [PATCH 04/28] fix: add Semantics wrappers to placeholder and circleAvatar builders - Wrap placeholder with Semantics(label: 'Placeholder') - Wrap circleAvatar with Semantics(label: label, image: imageUrl != null) Made-with: Cursor --- lib/presentation/widgets/server_misc.dart | 39 +++++++++++++---------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/lib/presentation/widgets/server_misc.dart b/lib/presentation/widgets/server_misc.dart index dadebdb..ae1c945 100644 --- a/lib/presentation/widgets/server_misc.dart +++ b/lib/presentation/widgets/server_misc.dart @@ -15,10 +15,13 @@ Widget buildServerPlaceholder( final width = (node.props['width'] as num?)?.toDouble() ?? 400; final height = (node.props['height'] as num?)?.toDouble() ?? 400; - return SizedBox( - width: width, - height: height, - child: Placeholder(color: color, strokeWidth: strokeWidth), + return Semantics( + label: 'Placeholder', + child: SizedBox( + width: width, + height: height, + child: Placeholder(color: color, strokeWidth: strokeWidth), + ), ); } @@ -34,18 +37,22 @@ Widget buildServerCircleAvatar( final label = node.props['label'] as String?; final icon = node.props['icon'] as String?; - return CircleAvatar( - radius: radius, - backgroundColor: bgColor, - foregroundColor: fgColor, - backgroundImage: imageUrl != null ? NetworkImage(imageUrl) : null, - child: imageUrl == null - ? (label != null - ? Text(label) - : icon != null - ? Icon(resolveIcon(icon)) - : null) - : null, + return Semantics( + label: label ?? 'Avatar', + image: imageUrl != null, + child: CircleAvatar( + radius: radius, + backgroundColor: bgColor, + foregroundColor: fgColor, + backgroundImage: imageUrl != null ? NetworkImage(imageUrl) : null, + child: imageUrl == null + ? (label != null + ? Text(label) + : icon != null + ? Icon(resolveIcon(icon)) + : null) + : null, + ), ); } From dc679064983099bf775313c476bafcf14c5eb9b7 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:49:23 -0300 Subject: [PATCH 05/28] feat: implement openUrl action with url_launcher - Replace snackbar placeholder with actual URL launching - Use launchUrl with LaunchMode.externalApplication Made-with: Cursor --- lib/presentation/widgets/server_button.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/presentation/widgets/server_button.dart b/lib/presentation/widgets/server_button.dart index 59db646..4c3f4d8 100644 --- a/lib/presentation/widgets/server_button.dart +++ b/lib/presentation/widgets/server_button.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../../core/models/screen_contract.dart'; import '../../core/utils/color_utils.dart'; @@ -65,9 +66,13 @@ void handleAction(BuildContext context, ActionDef? action) { const SnackBar(content: Text('Copied to clipboard')), ); case 'openUrl': - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Open URL: ${action.message ?? ''}')), - ); + final url = action.message ?? ''; + if (url.isNotEmpty) { + final uri = Uri.tryParse(url); + if (uri != null) { + launchUrl(uri, mode: LaunchMode.externalApplication); + } + } case 'showDialog': showDialog( context: context, From cb67803842f6f848814c079efd14a92d9c9a3108 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:49:31 -0300 Subject: [PATCH 06/28] chore: add url_launcher dependency for openUrl action - Add url_launcher ^6.3.2 to pubspec.yaml Made-with: Cursor --- pubspec.lock | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++- pubspec.yaml | 1 + 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index 464b547..cd55b3b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -88,6 +88,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -170,6 +175,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" process: dependency: transitive description: @@ -239,6 +252,70 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" + url: "https://pub.dev" + source: hosted + version: "6.3.29" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" vector_math: dependency: transitive description: @@ -255,6 +332,14 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" webdriver: dependency: transitive description: @@ -265,4 +350,4 @@ packages: version: "3.1.0" sdks: dart: ">=3.11.4 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.38.0" diff --git a/pubspec.yaml b/pubspec.yaml index a34b642..71c9d07 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: sdk: flutter cupertino_icons: ^1.0.8 + url_launcher: ^6.3.2 dev_dependencies: flutter_test: From d6f1f08af0b50ad1e4ede1951fe511ca5e420e4c Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:49:39 -0300 Subject: [PATCH 07/28] test: add unit tests for parsing_utils helpers - Cover parsePadding, parseAlignment, parseAxis, parseTextAlign - Cover parseFontWeight, parseFontStyle, parseTextDecoration - Cover parseBoxFit, parseBorderRadius, parseBoxConstraints - Cover parseDuration, parseCurve, parseScrollPhysics Made-with: Cursor --- test/core/utils/parsing_utils_test.dart | 167 ++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 test/core/utils/parsing_utils_test.dart diff --git a/test/core/utils/parsing_utils_test.dart b/test/core/utils/parsing_utils_test.dart new file mode 100644 index 0000000..310627e --- /dev/null +++ b/test/core/utils/parsing_utils_test.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_backend_driven_ui/core/utils/parsing_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('parsePadding', () { + test('null returns null', () { + expect(parsePadding(null), isNull); + }); + + test('num returns EdgeInsets.all', () { + expect(parsePadding(16), const EdgeInsets.all(16)); + }); + + test('map returns LTRB', () { + expect( + parsePadding({'left': 1, 'top': 2, 'right': 3, 'bottom': 4}), + const EdgeInsets.fromLTRB(1, 2, 3, 4), + ); + }); + }); + + group('parseAlignment', () { + test('known values', () { + expect(parseAlignment('topLeft'), Alignment.topLeft); + expect(parseAlignment('center'), Alignment.center); + expect(parseAlignment('bottomRight'), Alignment.bottomRight); + }); + + test('unknown defaults to topStart', () { + expect(parseAlignment('unknown'), AlignmentDirectional.topStart); + }); + + test('null defaults to topStart', () { + expect(parseAlignment(null), AlignmentDirectional.topStart); + }); + }); + + group('parseMainAxisAlignment', () { + test('known values', () { + expect(parseMainAxisAlignment('center'), MainAxisAlignment.center); + expect(parseMainAxisAlignment('spaceBetween'), MainAxisAlignment.spaceBetween); + }); + + test('null defaults to start', () { + expect(parseMainAxisAlignment(null), MainAxisAlignment.start); + }); + }); + + group('parseCrossAxisAlignment', () { + test('known values', () { + expect(parseCrossAxisAlignment('stretch'), CrossAxisAlignment.stretch); + expect(parseCrossAxisAlignment('end'), CrossAxisAlignment.end); + }); + + test('null defaults to start', () { + expect(parseCrossAxisAlignment(null), CrossAxisAlignment.start); + }); + }); + + group('parseAxis', () { + test('horizontal', () => expect(parseAxis('horizontal'), Axis.horizontal)); + test('vertical', () => expect(parseAxis('vertical'), Axis.vertical)); + test('null defaults to vertical', () => expect(parseAxis(null), Axis.vertical)); + }); + + group('parseTextAlign', () { + test('center', () => expect(parseTextAlign('center'), TextAlign.center)); + test('null returns null', () => expect(parseTextAlign(null), isNull)); + }); + + group('parseFontWeight', () { + test('bold', () => expect(parseFontWeight('bold'), FontWeight.w700)); + test('w400', () => expect(parseFontWeight('w400'), FontWeight.w400)); + test('null returns null', () => expect(parseFontWeight(null), isNull)); + }); + + group('parseFontStyle', () { + test('italic', () => expect(parseFontStyle('italic'), FontStyle.italic)); + test('null returns null', () => expect(parseFontStyle(null), isNull)); + }); + + group('parseTextDecoration', () { + test('underline', () => expect(parseTextDecoration('underline'), TextDecoration.underline)); + test('null returns null', () => expect(parseTextDecoration(null), isNull)); + }); + + group('parseTextOverflow', () { + test('ellipsis', () => expect(parseTextOverflow('ellipsis'), TextOverflow.ellipsis)); + test('null returns null', () => expect(parseTextOverflow(null), isNull)); + }); + + group('parseBoxFit', () { + test('cover', () => expect(parseBoxFit('cover'), BoxFit.cover)); + test('null defaults to contain', () => expect(parseBoxFit(null), BoxFit.contain)); + }); + + group('parseStackFit', () { + test('expand', () => expect(parseStackFit('expand'), StackFit.expand)); + test('null defaults to loose', () => expect(parseStackFit(null), StackFit.loose)); + }); + + group('parseClipBehavior', () { + test('antiAlias', () => expect(parseClipBehavior('antiAlias'), Clip.antiAlias)); + test('null defaults to hardEdge', () => expect(parseClipBehavior(null), Clip.hardEdge)); + }); + + group('parseBorderRadius', () { + test('null returns null', () => expect(parseBorderRadius(null), isNull)); + test('num returns circular', () { + expect(parseBorderRadius(8), BorderRadius.circular(8)); + }); + test('map returns per-corner', () { + final result = parseBorderRadius({'topLeft': 4, 'bottomRight': 8}); + expect(result, isNotNull); + }); + }); + + group('parseBoxConstraints', () { + test('null returns null', () => expect(parseBoxConstraints(null), isNull)); + test('parses min/max values', () { + final bc = parseBoxConstraints({'minWidth': 50, 'maxWidth': 200}); + expect(bc!.minWidth, 50); + expect(bc.maxWidth, 200); + }); + }); + + group('parseBoxDecoration', () { + test('null returns empty BoxDecoration', () { + expect(parseBoxDecoration(null), const BoxDecoration()); + }); + test('parses color', () { + final dec = parseBoxDecoration({'color': '#FF0000'}); + expect(dec.color, isNotNull); + }); + }); + + group('parseDuration', () { + test('num returns Duration', () { + expect(parseDuration(500), const Duration(milliseconds: 500)); + }); + test('non-num returns fallback', () { + expect(parseDuration('bad'), const Duration(milliseconds: 300)); + }); + }); + + group('parseCurve', () { + test('linear', () => expect(parseCurve('linear'), Curves.linear)); + test('bounceOut', () => expect(parseCurve('bounceOut'), Curves.bounceOut)); + test('null defaults to easeInOut', () => expect(parseCurve(null), Curves.easeInOut)); + }); + + group('parseScrollPhysics', () { + test('bouncing', () => expect(parseScrollPhysics('bouncing'), isA())); + test('null returns null', () => expect(parseScrollPhysics(null), isNull)); + }); + + group('parseVerticalDirection', () { + test('up', () => expect(parseVerticalDirection('up'), VerticalDirection.up)); + test('null defaults to down', () => expect(parseVerticalDirection(null), VerticalDirection.down)); + }); + + group('parseTextDirection', () { + test('rtl', () => expect(parseTextDirection('rtl'), TextDirection.rtl)); + test('null returns null', () => expect(parseTextDirection(null), isNull)); + }); +} From 30e88966fa9ee3311c016385fa9097f9bf4263a8 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:49:48 -0300 Subject: [PATCH 08/28] test: add widget tests for layout wrapper builders - Test center, align, padding, sizedBox, opacity, clipRRect - Test safeArea, rotatedBox, ignorePointer, offstage Made-with: Cursor --- .../widgets/server_layout_wrappers_test.dart | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 test/presentation/widgets/server_layout_wrappers_test.dart diff --git a/test/presentation/widgets/server_layout_wrappers_test.dart b/test/presentation/widgets/server_layout_wrappers_test.dart new file mode 100644 index 0000000..bdef7ef --- /dev/null +++ b/test/presentation/widgets/server_layout_wrappers_test.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_backend_driven_ui/core/models/screen_contract.dart'; +import 'package:flutter_backend_driven_ui/presentation/widgets/server_layout_wrappers.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Widget _wrap(Widget Function(BuildContext) builder) { + return MaterialApp( + home: Scaffold(body: Builder(builder: builder)), + ); +} + +Widget _childBuilder(ComponentNode c) => const Text('child'); + +void main() { + testWidgets('buildServerCenter wraps child in Center', (tester) async { + final node = ComponentNode( + type: 'center', + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerCenter(node, ctx, _childBuilder))); + + expect(find.byType(Center), findsOneWidget); + expect(find.text('child'), findsOneWidget); + }); + + testWidgets('buildServerAlign uses alignment prop', (tester) async { + final node = ComponentNode( + type: 'align', + props: const {'alignment': 'bottomRight'}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerAlign(node, ctx, _childBuilder))); + + final align = tester.widget(find.byType(Align)); + expect(align.alignment, Alignment.bottomRight); + }); + + testWidgets('buildServerPadding applies padding', (tester) async { + final node = ComponentNode( + type: 'padding', + props: const {'padding': 16}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerPadding(node, ctx, _childBuilder))); + + final padding = tester.widget(find.byType(Padding).first); + expect(padding.padding, const EdgeInsets.all(16)); + }); + + testWidgets('buildServerSizedBox applies width and height', (tester) async { + final node = ComponentNode( + type: 'sizedBox', + props: const {'width': 100, 'height': 50}, + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerSizedBox(node, ctx, _childBuilder))); + + final box = tester.widget(find.byType(SizedBox).first); + expect(box.width, 100); + expect(box.height, 50); + }); + + testWidgets('buildServerOpacity clamps opacity between 0 and 1', (tester) async { + final node = ComponentNode( + type: 'opacity', + props: const {'opacity': 0.5}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerOpacity(node, ctx, _childBuilder))); + + final opacity = tester.widget(find.byType(Opacity)); + expect(opacity.opacity, 0.5); + }); + + testWidgets('buildServerClipRRect applies borderRadius', (tester) async { + final node = ComponentNode( + type: 'clipRRect', + props: const {'borderRadius': 12}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerClipRRect(node, ctx, _childBuilder))); + + expect(find.byType(ClipRRect), findsOneWidget); + }); + + testWidgets('buildServerSafeArea passes inset flags', (tester) async { + final node = ComponentNode( + type: 'safeArea', + props: const {'top': true, 'bottom': false}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerSafeArea(node, ctx, _childBuilder))); + + final safeArea = tester.widget(find.byType(SafeArea)); + expect(safeArea.top, true); + expect(safeArea.bottom, false); + }); + + testWidgets('buildServerRotatedBox applies quarterTurns', (tester) async { + final node = ComponentNode( + type: 'rotatedBox', + props: const {'quarterTurns': 2}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerRotatedBox(node, ctx, _childBuilder))); + + final rotated = tester.widget(find.byType(RotatedBox)); + expect(rotated.quarterTurns, 2); + }); + + testWidgets('buildServerIgnorePointer renders with child', (tester) async { + final node = ComponentNode( + type: 'ignorePointer', + props: const {'ignoring': true}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerIgnorePointer(node, ctx, _childBuilder))); + + expect(find.byType(IgnorePointer), findsWidgets); + expect(find.text('child'), findsOneWidget); + }); + + testWidgets('buildServerOffstage renders', (tester) async { + final node = ComponentNode( + type: 'offstage', + props: const {'offstage': false}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerOffstage(node, ctx, _childBuilder))); + + expect(find.text('child'), findsOneWidget); + }); +} From 64b94f08f0ca40bfb81d7e434a8126f519029389 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:50:05 -0300 Subject: [PATCH 09/28] test: add widget tests for button variant builders - Test textButton, outlinedButton, iconButton, FAB rendering - Verify Semantics wrappers and label display Made-with: Cursor --- .../widgets/server_button_variants_test.dart | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 test/presentation/widgets/server_button_variants_test.dart diff --git a/test/presentation/widgets/server_button_variants_test.dart b/test/presentation/widgets/server_button_variants_test.dart new file mode 100644 index 0000000..de1a8c1 --- /dev/null +++ b/test/presentation/widgets/server_button_variants_test.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_backend_driven_ui/core/models/screen_contract.dart'; +import 'package:flutter_backend_driven_ui/presentation/widgets/server_button_variants.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Widget _wrap(Widget Function(BuildContext) builder) { + return MaterialApp( + home: Scaffold(body: Builder(builder: builder)), + ); +} + +Widget _childBuilder(ComponentNode c) => const SizedBox(); + +void main() { + testWidgets('buildServerTextButton renders label', (tester) async { + final node = ComponentNode( + type: 'textButton', + props: const {'label': 'Click Me'}, + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerTextButton(node, ctx, _childBuilder))); + + expect(find.text('Click Me'), findsOneWidget); + expect(find.byType(TextButton), findsOneWidget); + }); + + testWidgets('buildServerOutlinedButton renders label', (tester) async { + final node = ComponentNode( + type: 'outlinedButton', + props: const {'label': 'Outline'}, + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerOutlinedButton(node, ctx, _childBuilder))); + + expect(find.text('Outline'), findsOneWidget); + expect(find.byType(OutlinedButton), findsOneWidget); + }); + + testWidgets('buildServerIconButton renders icon', (tester) async { + final node = ComponentNode( + type: 'iconButton', + props: const {'icon': 'home', 'tooltip': 'Home'}, + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerIconButton(node, ctx, _childBuilder))); + + expect(find.byType(IconButton), findsOneWidget); + expect(find.byIcon(Icons.home), findsOneWidget); + }); + + testWidgets('buildServerFab renders FloatingActionButton', (tester) async { + final node = ComponentNode( + type: 'floatingActionButton', + props: const {'icon': 'add'}, + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerFab(node, ctx, _childBuilder))); + + expect(find.byType(FloatingActionButton), findsOneWidget); + }); + + testWidgets('buildServerFab extended renders label and icon', (tester) async { + final node = ComponentNode( + type: 'floatingActionButton', + props: const {'icon': 'add', 'label': 'New Item'}, + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerFab(node, ctx, _childBuilder))); + + expect(find.text('New Item'), findsOneWidget); + expect(find.byType(FloatingActionButton), findsOneWidget); + }); + + testWidgets('textButton has Semantics wrapper', (tester) async { + final node = ComponentNode( + type: 'textButton', + props: const {'label': 'Accessible'}, + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerTextButton(node, ctx, _childBuilder))); + + expect(find.byType(Semantics), findsWidgets); + }); +} From 3386954b57b3d1aa9c6cd73a5f7df4605326f2d0 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:50:05 -0300 Subject: [PATCH 10/28] test: add widget tests for tile builders - Test listTile, expansionTile, switchListTile, checkboxListTile - Verify toggle callbacks report correct id and value Made-with: Cursor --- .../widgets/server_tiles_test.dart | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 test/presentation/widgets/server_tiles_test.dart diff --git a/test/presentation/widgets/server_tiles_test.dart b/test/presentation/widgets/server_tiles_test.dart new file mode 100644 index 0000000..844e8eb --- /dev/null +++ b/test/presentation/widgets/server_tiles_test.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_backend_driven_ui/core/models/screen_contract.dart'; +import 'package:flutter_backend_driven_ui/presentation/widgets/server_tiles.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Widget _wrap(Widget Function(BuildContext) builder) { + return MaterialApp( + home: Scaffold(body: Builder(builder: builder)), + ); +} + +Widget _childBuilder(ComponentNode c) => const Text('child'); + +void main() { + testWidgets('buildServerListTile renders title and subtitle', (tester) async { + final node = ComponentNode( + type: 'listTile', + props: const {'title': 'Title', 'subtitle': 'Subtitle', 'leadingIcon': 'home'}, + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerListTile(node, ctx, _childBuilder))); + + expect(find.text('Title'), findsOneWidget); + expect(find.text('Subtitle'), findsOneWidget); + expect(find.byType(ListTile), findsOneWidget); + }); + + testWidgets('buildServerExpansionTile renders title and children', (tester) async { + final node = ComponentNode( + type: 'expansionTile', + props: const {'title': 'Expand Me', 'initiallyExpanded': true}, + children: [const ComponentNode(type: 'text', props: {'content': 'Inner'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerExpansionTile(node, ctx, _childBuilder))); + + expect(find.text('Expand Me'), findsOneWidget); + expect(find.byType(ExpansionTile), findsOneWidget); + }); + + testWidgets('buildServerSwitchListTile renders and toggles', (tester) async { + String? seenId; + String? seenValue; + + final node = ComponentNode( + type: 'switchListTile', + id: 'sw1', + props: const {'title': 'Toggle', 'value': false}, + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerSwitchListTile( + node, ctx, _childBuilder, + onChanged: (id, val) { + seenId = id; + seenValue = val; + }, + ))); + + expect(find.text('Toggle'), findsOneWidget); + + await tester.tap(find.byType(Switch)); + await tester.pump(); + + expect(seenId, 'sw1'); + expect(seenValue, 'true'); + }); + + testWidgets('buildServerCheckboxListTile renders and toggles', (tester) async { + String? seenId; + + final node = ComponentNode( + type: 'checkboxListTile', + id: 'cb1', + props: const {'title': 'Check Me', 'value': false}, + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerCheckboxListTile( + node, ctx, _childBuilder, + onChanged: (id, val) => seenId = id, + ))); + + expect(find.text('Check Me'), findsOneWidget); + + await tester.tap(find.byType(Checkbox)); + await tester.pump(); + + expect(seenId, 'cb1'); + }); +} From 23c6d2c3df12dd8b9df82a192450bf84b60a1119 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:50:05 -0300 Subject: [PATCH 11/28] test: add widget tests for text variant builders - Test selectableText, richText rendering and Semantics Made-with: Cursor --- .../widgets/server_text_variants_test.dart | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 test/presentation/widgets/server_text_variants_test.dart diff --git a/test/presentation/widgets/server_text_variants_test.dart b/test/presentation/widgets/server_text_variants_test.dart new file mode 100644 index 0000000..a114bb2 --- /dev/null +++ b/test/presentation/widgets/server_text_variants_test.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_backend_driven_ui/core/models/screen_contract.dart'; +import 'package:flutter_backend_driven_ui/presentation/widgets/server_text_variants.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Widget _wrap(Widget Function(BuildContext) builder) { + return MaterialApp( + home: Scaffold(body: Builder(builder: builder)), + ); +} + +Widget _childBuilder(ComponentNode c) => const SizedBox(); + +void main() { + testWidgets('buildServerSelectableText renders selectable content', (tester) async { + final node = ComponentNode( + type: 'selectableText', + props: const {'content': 'Select me'}, + ); + + await tester.pumpWidget( + _wrap((ctx) => buildServerSelectableText(node, ctx, _childBuilder))); + + expect(find.text('Select me'), findsOneWidget); + expect(find.byType(SelectableText), findsOneWidget); + }); + + testWidgets('buildServerRichText renders spans', (tester) async { + final node = ComponentNode( + type: 'richText', + props: { + 'spans': [ + {'text': 'Hello ', 'fontWeight': 'bold'}, + {'text': 'World', 'color': '#FF0000'}, + ], + }, + ); + + await tester.pumpWidget( + _wrap((ctx) => buildServerRichText(node, ctx, _childBuilder))); + + expect(find.byType(RichText), findsWidgets); + }); + + testWidgets('buildServerSelectableText has Semantics', (tester) async { + final node = ComponentNode( + type: 'selectableText', + props: const {'content': 'Accessible'}, + ); + + await tester.pumpWidget( + _wrap((ctx) => buildServerSelectableText(node, ctx, _childBuilder))); + + expect(find.byType(Semantics), findsWidgets); + }); +} From 04e39bde5e92162b17a4a9381232e8723b23d626 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:50:06 -0300 Subject: [PATCH 12/28] test: add widget tests for input variant builders - Test slider rendering and value clamping - Test radio widget rendering Made-with: Cursor --- .../widgets/server_input_variants_test.dart | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 test/presentation/widgets/server_input_variants_test.dart diff --git a/test/presentation/widgets/server_input_variants_test.dart b/test/presentation/widgets/server_input_variants_test.dart new file mode 100644 index 0000000..056c484 --- /dev/null +++ b/test/presentation/widgets/server_input_variants_test.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_backend_driven_ui/core/models/screen_contract.dart'; +import 'package:flutter_backend_driven_ui/presentation/widgets/server_input_variants.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Widget _wrap(Widget Function(BuildContext) builder) { + return MaterialApp( + home: Scaffold(body: Builder(builder: builder)), + ); +} + +Widget _childBuilder(ComponentNode c) => const SizedBox(); + +void main() { + testWidgets('buildServerSlider renders Slider widget', (tester) async { + final node = ComponentNode( + type: 'slider', + id: 'volume', + props: const {'value': 0.5, 'min': 0, 'max': 1}, + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerSlider( + node, ctx, _childBuilder, + ))); + + expect(find.byType(Slider), findsOneWidget); + + final slider = tester.widget(find.byType(Slider)); + expect(slider.value, 0.5); + expect(slider.min, 0); + expect(slider.max, 1); + }); + + testWidgets('buildServerSlider clamps initial value to min/max', (tester) async { + final node = ComponentNode( + type: 'slider', + id: 's1', + props: const {'value': 5, 'min': 0, 'max': 1}, + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerSlider( + node, ctx, _childBuilder, + ))); + + final slider = tester.widget(find.byType(Slider)); + expect(slider.value, 1.0); + }); + + testWidgets('buildServerRadio renders Radio widget', (tester) async { + final node = ComponentNode( + type: 'radio', + id: 'r1', + props: const {'value': 'a', 'groupValue': 'a'}, + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerRadio( + node, ctx, _childBuilder, + ))); + + expect(find.byType(Radio), findsOneWidget); + }); +} From c3cb3da5b19f7a4add6a46e388326a91ca96545d Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:50:06 -0300 Subject: [PATCH 13/28] test: add widget tests for miscellaneous builders - Test placeholder, circleAvatar, verticalDivider - Verify Semantics wrappers on placeholder and avatar Made-with: Cursor --- .../widgets/server_misc_test.dart | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 test/presentation/widgets/server_misc_test.dart diff --git a/test/presentation/widgets/server_misc_test.dart b/test/presentation/widgets/server_misc_test.dart new file mode 100644 index 0000000..686211f --- /dev/null +++ b/test/presentation/widgets/server_misc_test.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_backend_driven_ui/core/models/screen_contract.dart'; +import 'package:flutter_backend_driven_ui/presentation/widgets/server_misc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Widget _wrap(Widget Function(BuildContext) builder) { + return MaterialApp( + home: Scaffold(body: Builder(builder: builder)), + ); +} + +Widget _childBuilder(ComponentNode c) => const SizedBox(); + +void main() { + testWidgets('buildServerPlaceholder renders Placeholder widget', (tester) async { + final node = ComponentNode( + type: 'placeholder', + props: const {'width': 200, 'height': 100}, + ); + + await tester.pumpWidget( + _wrap((ctx) => buildServerPlaceholder(node, ctx, _childBuilder))); + + expect(find.byType(Placeholder), findsOneWidget); + }); + + testWidgets('buildServerCircleAvatar renders with label', (tester) async { + final node = ComponentNode( + type: 'circleAvatar', + props: const {'label': 'JD', 'radius': 24}, + ); + + await tester.pumpWidget( + _wrap((ctx) => buildServerCircleAvatar(node, ctx, _childBuilder))); + + expect(find.byType(CircleAvatar), findsOneWidget); + expect(find.text('JD'), findsOneWidget); + }); + + testWidgets('buildServerCircleAvatar renders with icon', (tester) async { + final node = ComponentNode( + type: 'circleAvatar', + props: const {'icon': 'person', 'radius': 24}, + ); + + await tester.pumpWidget( + _wrap((ctx) => buildServerCircleAvatar(node, ctx, _childBuilder))); + + expect(find.byType(CircleAvatar), findsOneWidget); + expect(find.byIcon(Icons.person), findsOneWidget); + }); + + testWidgets('buildServerVerticalDivider renders', (tester) async { + final node = ComponentNode( + type: 'verticalDivider', + props: const {'width': 16, 'thickness': 2}, + ); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Row( + children: [ + Builder( + builder: (ctx) => buildServerVerticalDivider(node, ctx, _childBuilder), + ), + ], + ), + ), + )); + + expect(find.byType(VerticalDivider), findsOneWidget); + }); + + testWidgets('buildServerPlaceholder has Semantics', (tester) async { + final node = ComponentNode( + type: 'placeholder', + props: const {'width': 100, 'height': 100}, + ); + + await tester.pumpWidget( + _wrap((ctx) => buildServerPlaceholder(node, ctx, _childBuilder))); + + expect(find.byType(Semantics), findsWidgets); + expect(find.byType(Placeholder), findsOneWidget); + }); +} From 4b2beeefeea0b893fefd64b79767988afe3f3a3f Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:50:06 -0300 Subject: [PATCH 14/28] test: add widget tests for interactive builders - Test inkWell, gestureDetector, tooltip, dismissible - Verify Semantics and action dispatch on tap Made-with: Cursor --- .../widgets/server_interactives_test.dart | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 test/presentation/widgets/server_interactives_test.dart diff --git a/test/presentation/widgets/server_interactives_test.dart b/test/presentation/widgets/server_interactives_test.dart new file mode 100644 index 0000000..81dae7d --- /dev/null +++ b/test/presentation/widgets/server_interactives_test.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_backend_driven_ui/core/models/screen_contract.dart'; +import 'package:flutter_backend_driven_ui/presentation/widgets/server_interactives.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Widget _wrap(Widget Function(BuildContext) builder) { + return MaterialApp( + home: Scaffold(body: Builder(builder: builder)), + ); +} + +Widget _childBuilder(ComponentNode c) => const Text('child'); + +void main() { + testWidgets('buildServerInkWell renders child and responds to tap', (tester) async { + final node = ComponentNode( + type: 'inkWell', + action: const ActionDef(type: 'snackbar', message: 'tapped'), + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerInkWell(node, ctx, _childBuilder))); + + expect(find.text('child'), findsOneWidget); + expect(find.byType(InkWell), findsOneWidget); + + await tester.tap(find.byType(InkWell)); + await tester.pump(); + + expect(find.byType(SnackBar), findsOneWidget); + }); + + testWidgets('buildServerGestureDetector renders and fires action', (tester) async { + final node = ComponentNode( + type: 'gestureDetector', + action: const ActionDef(type: 'snackbar', message: 'detected'), + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester + .pumpWidget(_wrap((ctx) => buildServerGestureDetector(node, ctx, _childBuilder))); + + await tester.tap(find.text('child')); + await tester.pump(); + + expect(find.byType(SnackBar), findsOneWidget); + }); + + testWidgets('buildServerTooltip renders with message', (tester) async { + final node = ComponentNode( + type: 'tooltip', + props: const {'message': 'Help text'}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerTooltip(node, ctx, _childBuilder))); + + expect(find.byType(Tooltip), findsOneWidget); + }); + + testWidgets('buildServerInkWell has Semantics wrapper', (tester) async { + final node = ComponentNode( + type: 'inkWell', + action: const ActionDef(type: 'snackbar', message: 'tap'), + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerInkWell(node, ctx, _childBuilder))); + + expect(find.byType(Semantics), findsWidgets); + }); + + testWidgets('buildServerDismissible renders child with key', (tester) async { + final node = ComponentNode( + type: 'dismissible', + id: 'item_1', + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester + .pumpWidget(_wrap((ctx) => buildServerDismissible(node, ctx, _childBuilder))); + + expect(find.byType(Dismissible), findsOneWidget); + expect(find.text('child'), findsOneWidget); + }); +} From 30229b08c0ac2d821616caef0258fdf0aea11327 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:50:06 -0300 Subject: [PATCH 15/28] test: add widget tests for scrollable builders - Test scrollView, gridView, pageView rendering Made-with: Cursor --- .../widgets/server_scrollables_test.dart | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 test/presentation/widgets/server_scrollables_test.dart diff --git a/test/presentation/widgets/server_scrollables_test.dart b/test/presentation/widgets/server_scrollables_test.dart new file mode 100644 index 0000000..4d9d6d5 --- /dev/null +++ b/test/presentation/widgets/server_scrollables_test.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_backend_driven_ui/core/models/screen_contract.dart'; +import 'package:flutter_backend_driven_ui/presentation/widgets/server_scrollables.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Widget _wrap(Widget Function(BuildContext) builder) { + return MaterialApp( + home: Scaffold(body: Builder(builder: builder)), + ); +} + +Widget _childBuilder(ComponentNode c) => const SizedBox(height: 50, child: Text('item')); + +void main() { + testWidgets('buildServerScrollView renders SingleChildScrollView', (tester) async { + final node = ComponentNode( + type: 'scrollView', + children: [ + const ComponentNode(type: 'text', props: {'content': 'a'}), + const ComponentNode(type: 'text', props: {'content': 'b'}), + ], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerScrollView(node, ctx, _childBuilder))); + + expect(find.byType(SingleChildScrollView), findsOneWidget); + expect(find.text('item'), findsNWidgets(2)); + }); + + testWidgets('buildServerGridView renders with crossAxisCount', (tester) async { + final node = ComponentNode( + type: 'gridView', + props: const {'crossAxisCount': 3, 'crossAxisSpacing': 8, 'mainAxisSpacing': 8}, + children: List.generate( + 6, + (_) => const ComponentNode(type: 'text', props: {'content': 'cell'}), + ), + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerGridView(node, ctx, _childBuilder))); + + expect(find.byType(GridView), findsOneWidget); + expect(find.text('item'), findsNWidgets(6)); + }); + + testWidgets('buildServerPageView renders PageView with height', (tester) async { + final node = ComponentNode( + type: 'pageView', + props: const {'height': 150}, + children: [ + const ComponentNode(type: 'text', props: {'content': 'page1'}), + const ComponentNode(type: 'text', props: {'content': 'page2'}), + ], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerPageView(node, ctx, _childBuilder))); + + expect(find.byType(PageView), findsOneWidget); + }); +} From a8b34f13f68385b76dc235ff0ca06f766d1fded5 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:50:06 -0300 Subject: [PATCH 16/28] test: add widget tests for decorator builders - Test material, hero, indexedStack, transform, banner Made-with: Cursor --- .../widgets/server_decorators_test.dart | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 test/presentation/widgets/server_decorators_test.dart diff --git a/test/presentation/widgets/server_decorators_test.dart b/test/presentation/widgets/server_decorators_test.dart new file mode 100644 index 0000000..3b46afa --- /dev/null +++ b/test/presentation/widgets/server_decorators_test.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_backend_driven_ui/core/models/screen_contract.dart'; +import 'package:flutter_backend_driven_ui/presentation/widgets/server_decorators.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Widget _wrap(Widget Function(BuildContext) builder) { + return MaterialApp( + home: Scaffold(body: Builder(builder: builder)), + ); +} + +Widget _childBuilder(ComponentNode c) => const Text('child'); + +void main() { + testWidgets('buildServerMaterial renders Material widget', (tester) async { + final node = ComponentNode( + type: 'material', + props: const {'elevation': 4}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerMaterial(node, ctx, _childBuilder))); + + expect(find.text('child'), findsOneWidget); + }); + + testWidgets('buildServerHero renders Hero with tag', (tester) async { + final node = ComponentNode( + type: 'hero', + props: const {'tag': 'avatar'}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerHero(node, ctx, _childBuilder))); + + final hero = tester.widget(find.byType(Hero)); + expect(hero.tag, 'avatar'); + }); + + testWidgets('buildServerIndexedStack shows child at index', (tester) async { + final node = ComponentNode( + type: 'indexedStack', + props: const {'index': 0}, + children: [ + const ComponentNode(type: 'text', props: {'content': 'first'}), + const ComponentNode(type: 'text', props: {'content': 'second'}), + ], + ); + + await tester + .pumpWidget(_wrap((ctx) => buildServerIndexedStack(node, ctx, _childBuilder))); + + expect(find.byType(IndexedStack), findsOneWidget); + }); + + testWidgets('buildServerTransform rotate renders Transform', (tester) async { + final node = ComponentNode( + type: 'transform', + props: const {'transformType': 'rotate', 'angle': 1.57}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerTransform(node, ctx, _childBuilder))); + + expect(find.text('child'), findsOneWidget); + }); + + testWidgets('buildServerTransform scale renders child', (tester) async { + final node = ComponentNode( + type: 'transform', + props: const {'transformType': 'scale', 'scale': 2.0}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerTransform(node, ctx, _childBuilder))); + + expect(find.text('child'), findsOneWidget); + }); + + testWidgets('buildServerBanner renders child with banner overlay', (tester) async { + final node = ComponentNode( + type: 'banner', + props: const {'message': 'BETA', 'location': 'topEnd'}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerBanner(node, ctx, _childBuilder))); + + expect(find.text('child'), findsOneWidget); + }); +} From d676447dca53f9458fe5916057064f7378ae7044 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:50:07 -0300 Subject: [PATCH 17/28] test: add widget tests for animated widget builders - Test animatedContainer, animatedOpacity, animatedCrossFade - Test animatedSwitcher and animatedSize rendering Made-with: Cursor --- .../widgets/server_animated_widgets_test.dart | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 test/presentation/widgets/server_animated_widgets_test.dart diff --git a/test/presentation/widgets/server_animated_widgets_test.dart b/test/presentation/widgets/server_animated_widgets_test.dart new file mode 100644 index 0000000..e0e844d --- /dev/null +++ b/test/presentation/widgets/server_animated_widgets_test.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_backend_driven_ui/core/models/screen_contract.dart'; +import 'package:flutter_backend_driven_ui/presentation/widgets/server_animated_widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Widget _wrap(Widget Function(BuildContext) builder) { + return MaterialApp( + home: Scaffold(body: Builder(builder: builder)), + ); +} + +Widget _childBuilder(ComponentNode c) => const Text('child'); + +void main() { + testWidgets('buildServerAnimatedContainer renders', (tester) async { + final node = ComponentNode( + type: 'animatedContainer', + props: const {'duration': 300, 'width': 100, 'height': 100}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester + .pumpWidget(_wrap((ctx) => buildServerAnimatedContainer(node, ctx, _childBuilder))); + + expect(find.byType(AnimatedContainer), findsOneWidget); + }); + + testWidgets('buildServerAnimatedOpacity renders with opacity', (tester) async { + final node = ComponentNode( + type: 'animatedOpacity', + props: const {'opacity': 0.5, 'duration': 200}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester + .pumpWidget(_wrap((ctx) => buildServerAnimatedOpacity(node, ctx, _childBuilder))); + + final widget = tester.widget(find.byType(AnimatedOpacity)); + expect(widget.opacity, 0.5); + }); + + testWidgets('buildServerAnimatedCrossFade shows first child', (tester) async { + final node = ComponentNode( + type: 'animatedCrossFade', + props: const {'showFirst': true, 'duration': 200}, + children: [ + const ComponentNode(type: 'text', props: {'content': 'first'}), + const ComponentNode(type: 'text', props: {'content': 'second'}), + ], + ); + + await tester + .pumpWidget(_wrap((ctx) => buildServerAnimatedCrossFade(node, ctx, _childBuilder))); + + expect(find.byType(AnimatedCrossFade), findsOneWidget); + }); + + testWidgets('buildServerAnimatedSwitcher renders', (tester) async { + final node = ComponentNode( + type: 'animatedSwitcher', + props: const {'duration': 300}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester + .pumpWidget(_wrap((ctx) => buildServerAnimatedSwitcher(node, ctx, _childBuilder))); + + expect(find.byType(AnimatedSwitcher), findsOneWidget); + }); + + testWidgets('buildServerAnimatedSize renders', (tester) async { + final node = ComponentNode( + type: 'animatedSize', + props: const {'duration': 300}, + children: [const ComponentNode(type: 'text', props: {'content': 'hi'})], + ); + + await tester + .pumpWidget(_wrap((ctx) => buildServerAnimatedSize(node, ctx, _childBuilder))); + + expect(find.byType(AnimatedSize), findsOneWidget); + }); +} From 6d8214a14517e37fc574c49b051badcd67842739 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:50:07 -0300 Subject: [PATCH 18/28] test: add widget tests for table builders - Test table with rows and dataTable with columns and rows Made-with: Cursor --- .../widgets/server_tables_test.dart | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 test/presentation/widgets/server_tables_test.dart diff --git a/test/presentation/widgets/server_tables_test.dart b/test/presentation/widgets/server_tables_test.dart new file mode 100644 index 0000000..e238afa --- /dev/null +++ b/test/presentation/widgets/server_tables_test.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_backend_driven_ui/core/models/screen_contract.dart'; +import 'package:flutter_backend_driven_ui/presentation/widgets/server_tables.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Widget _wrap(Widget Function(BuildContext) builder) { + return MaterialApp( + home: Scaffold(body: Builder(builder: builder)), + ); +} + +Widget _childBuilder(ComponentNode c) => Text(c.props['content'] as String? ?? 'cell'); + +void main() { + testWidgets('buildServerTable renders Table with rows', (tester) async { + final node = ComponentNode( + type: 'table', + children: [ + ComponentNode( + type: 'tableRow', + children: [ + const ComponentNode(type: 'text', props: {'content': 'A'}), + const ComponentNode(type: 'text', props: {'content': 'B'}), + ], + ), + ], + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerTable(node, ctx, _childBuilder))); + + expect(find.byType(Table), findsOneWidget); + }); + + testWidgets('buildServerDataTable renders columns and rows', (tester) async { + final node = ComponentNode( + type: 'dataTable', + props: { + 'columns': [ + {'label': 'Name'}, + {'label': 'Age', 'numeric': true}, + ], + 'rows': [ + { + 'cells': [ + {'value': 'Alice'}, + {'value': '30'}, + ], + }, + { + 'cells': [ + {'value': 'Bob'}, + {'value': '25'}, + ], + }, + ], + }, + ); + + await tester.pumpWidget(_wrap((ctx) => buildServerDataTable(node, ctx, _childBuilder))); + + expect(find.byType(DataTable), findsOneWidget); + expect(find.text('Name'), findsOneWidget); + expect(find.text('Age'), findsOneWidget); + expect(find.text('Alice'), findsOneWidget); + expect(find.text('Bob'), findsOneWidget); + }); +} From bbb1da8df65abf8e7a862e2248072b5c9aaf0138 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:50:23 -0300 Subject: [PATCH 19/28] docs: create COMPONENTS.md reference catalog - Full listing of all 103 component types with props and children support - Organized by category: layout, wrappers, decorators, scrollables, etc. - Include action types reference table Made-with: Cursor --- docs/COMPONENTS.md | 209 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 docs/COMPONENTS.md diff --git a/docs/COMPONENTS.md b/docs/COMPONENTS.md new file mode 100644 index 0000000..31191c2 --- /dev/null +++ b/docs/COMPONENTS.md @@ -0,0 +1,209 @@ +# Component Reference Catalog + +This document lists every supported component type, its JSON `type` key, accepted `props`, and whether it supports `children`. + +--- + +## Core Layout (12) + +| Type | Children | Key Props | +|------|----------|-----------| +| `column` | yes | `mainAxisAlignment`, `crossAxisAlignment`, `padding` | +| `row` | yes | `mainAxisAlignment`, `crossAxisAlignment`, `padding` | +| `container` | yes | `padding`, `decoration`, `backgroundColor`, `width`, `height` | +| `card` | yes | `padding`, `elevation` | +| `listView` | yes | `padding` | +| `stack` | yes | `alignment`, `fit` | +| `positioned` | yes (1) | `top`, `bottom`, `left`, `right` | +| `wrap` | yes | `spacing`, `runSpacing`, `alignment`, `padding` | +| `spacer` | no | `height`, `width` | +| `responsive` | yes | breakpoint-specific children | +| `expanded` | yes (1) | `flex` | +| `flexible` | yes (1) | `flex`, `fit` | + +--- + +## Layout Wrappers (22) + +| Type | Children | Key Props | +|------|----------|-----------| +| `center` | yes (1) | `widthFactor`, `heightFactor` | +| `align` | yes (1) | `alignment` | +| `padding` | yes (1) | `padding` | +| `sizedBox` | yes (1) | `width`, `height` | +| `constrainedBox` | yes (1) | `constraints` (`minWidth`, `maxWidth`, `minHeight`, `maxHeight`) | +| `fittedBox` | yes (1) | `fit`, `alignment` | +| `fractionallySizedBox` | yes (1) | `widthFactor`, `heightFactor`, `alignment` | +| `intrinsicHeight` | yes (1) | — | +| `intrinsicWidth` | yes (1) | — | +| `limitedBox` | yes (1) | `maxWidth`, `maxHeight` | +| `overflowBox` | yes (1) | `maxWidth`, `maxHeight`, `alignment` | +| `aspectRatio` | yes (1) | `aspectRatio` | +| `baseline` | yes (1) | `baseline`, `baselineType` | +| `opacity` | yes (1) | `opacity` (0.0–1.0) | +| `clipRRect` | yes (1) | `borderRadius` | +| `clipOval` | yes (1) | — | +| `safeArea` | yes (1) | `top`, `bottom`, `left`, `right` | +| `rotatedBox` | yes (1) | `quarterTurns` | +| `ignorePointer` | yes (1) | `ignoring` | +| `absorbPointer` | yes (1) | `absorbing` | +| `offstage` | yes (1) | `offstage` | +| `visibility` | yes (1) | `visible` | + +--- + +## Decorators (7) + +| Type | Children | Key Props | +|------|----------|-----------| +| `material` | yes (1) | `elevation`, `color`, `borderRadius`, `type` | +| `hero` | yes (1) | `tag` | +| `decoratedBox` | yes (1) | `decoration` (BoxDecoration object) | +| `indexedStack` | yes | `index` | +| `transform` | yes (1) | `transformType` (`rotate`/`scale`/`translate`), `angle`, `scale`, `offsetX`, `offsetY` | +| `backdropFilter` | yes (1) | `sigmaX`, `sigmaY` | +| `banner` | yes (1) | `message`, `location` (`topStart`/`topEnd`/`bottomStart`/`bottomEnd`), `color` | + +--- + +## Scrollables (6) + +| Type | Children | Key Props | +|------|----------|-----------| +| `scrollView` | yes | `direction`, `reverse`, `padding`, `physics` | +| `gridView` | yes | `crossAxisCount`, `crossAxisSpacing`, `mainAxisSpacing`, `childAspectRatio`, `padding` | +| `pageView` | yes | `height`, `scrollDirection`, `pageSnapping`, `physics` | +| `customScrollView` | yes | `physics` | +| `sliverList` | yes | — | +| `sliverGrid` | yes | `crossAxisCount`, `crossAxisSpacing`, `mainAxisSpacing`, `childAspectRatio` | + +--- + +## Interactives (6) + +| Type | Children | Key Props | +|------|----------|-----------| +| `inkWell` | yes (1) | `borderRadius`, `splashColor`, `highlightColor` + `action` | +| `gestureDetector` | yes (1) | + `action` | +| `tooltip` | yes (1) | `message` | +| `dismissible` | yes (1) | `id` (used as Key), `direction` | +| `draggable` | yes (1) | `axis`, `feedbackScale` | +| `longPressDraggable` | yes (1) | `axis`, `feedbackScale` | + +--- + +## Animated (9) + +All animated types accept `duration` (ms) and `curve`. + +| Type | Children | Extra Props | +|------|----------|-------------| +| `animatedContainer` | yes (1) | `width`, `height`, `padding`, `decoration`, `alignment` | +| `animatedOpacity` | yes (1) | `opacity` | +| `animatedCrossFade` | yes (2) | `showFirst` | +| `animatedSwitcher` | yes (1) | — | +| `animatedAlign` | yes (1) | `alignment` | +| `animatedPadding` | yes (1) | `padding` | +| `animatedPositioned` | yes (1) | `top`, `bottom`, `left`, `right`, `width`, `height` | +| `animatedSize` | yes (1) | `alignment` | +| `animatedScale` | yes (1) | `scale`, `alignment` | + +--- + +## Tiles (5) + +| Type | Children | Key Props | +|------|----------|-----------| +| `listTile` | no | `title`, `subtitle`, `leadingIcon`, `trailingIcon` | +| `expansionTile` | yes | `title`, `subtitle`, `leadingIcon`, `initiallyExpanded` | +| `switchListTile` | no | `id`, `title`, `subtitle`, `value` | +| `checkboxListTile` | no | `id`, `title`, `subtitle`, `value` | +| `radioListTile` | no | `id`, `title`, `subtitle`, `value`, `groupValue` | + +--- + +## Tables (4) + +| Type | Children | Key Props | +|------|----------|-----------| +| `table` | yes (`tableRow`) | `border`, `defaultVerticalAlignment`, `columnWidths` | +| `tableRow` | yes | — | +| `tableCell` | yes (1) | `verticalAlignment` | +| `dataTable` | no | `columns` (array), `rows` (array with `cells`) | + +--- + +## Text Variants (3) + +| Type | Children | Key Props | +|------|----------|-----------| +| `selectableText` | no | `content`, `style`, `textAlign`, `maxLines` | +| `richText` | no | `spans` (array: `text`, `color`, `fontSize`, `fontWeight`, `fontStyle`, `decoration`) | +| `defaultTextStyle` | yes | `style` | + +--- + +## Button Variants (5) + +| Type | Children | Key Props | +|------|----------|-----------| +| `textButton` | no | `label`, `icon`, `color` + `action` | +| `outlinedButton` | no | `label`, `icon`, `color`, `borderColor` + `action` | +| `iconButton` | no | `icon`, `tooltip`, `size`, `color` + `action` | +| `floatingActionButton` | no | `icon`, `label` (extended), `backgroundColor` + `action` | +| `segmentedButton` | no | `segments` (array: `value`, `label`, `icon`), `selected` | + +--- + +## Media & Display (7) + +| Type | Children | Key Props | +|------|----------|-----------| +| `placeholder` | no | `width`, `height`, `strokeWidth`, `color` | +| `circleAvatar` | no | `label`, `icon`, `imageUrl`, `radius`, `backgroundColor` | +| `verticalDivider` | no | `width`, `thickness`, `color`, `indent`, `endIndent` | +| `popupMenuButton` | no | `icon`, `items` (array: `value`, `label`, `icon`) + `action` | +| `searchBar` | no | `hintText`, `leadingIcon` | +| `searchAnchor` | no | `hintText`, `suggestions` (array) | +| `tooltip` | yes (1) | `message` | + +--- + +## Leaf Components (10) + +| Type | Children | Key Props | +|------|----------|-----------| +| `text` | no | `content`, `style` (`fontSize`, `fontWeight`, `color`, `textAlign`) | +| `button` | no | `label`, `style` (`backgroundColor`, `textColor`, `borderRadius`) + `action` | +| `image` | no | `url`, `width`, `height`, `fit`, `borderRadius` | +| `input` | no | `id`, `label`, `hint`, `maxLines`, `keyboardType`, `validation` | +| `divider` | no | `height`, `thickness`, `color`, `indent`, `endIndent` | +| `icon` | no | `name`, `size`, `color` | +| `chip` | no | `label`, `avatar`, `backgroundColor`, `textColor`, `outlined` | +| `progress` | no | `variant` (`linear`/`circular`), `value`, `color`, `strokeWidth` | +| `badge` | yes (1) | `label`, `backgroundColor`, `textColor`, `small` | +| `switch` / `checkbox` | no | `id`, `label`, `subtitle`, `value` | + +--- + +## Interactive Inputs (3) + +| Type | Children | Key Props | +|------|----------|-----------| +| `slider` | no | `id`, `value`, `min`, `max`, `divisions`, `label`, `activeColor` | +| `rangeSlider` | no | `id`, `startValue`, `endValue`, `min`, `max`, `divisions`, `activeColor` | +| `radio` | no | `id`, `value`, `groupValue`, `label`, `activeColor` | + +--- + +## Action Types (7) + +| Action | Required Fields | Description | +|--------|----------------|-------------| +| `navigate` | `targetScreenId` | Push a new screen | +| `goBack` | — | Pop current screen | +| `snackbar` | `message` | Show a snackbar | +| `submit` | — | Collect all input values | +| `copyToClipboard` | `message` | Copy text to clipboard | +| `openUrl` | `message` (URL) | Open URL in external browser | +| `showDialog` | `targetScreenId` (title), `message` (body) | Show an alert dialog | From 4861e0c44537ae65068cf9f065e75631f41f1b87 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:50:24 -0300 Subject: [PATCH 20/28] docs: update ARCHITECTURE.md with expanded component catalog - Add new component categories: wrappers, decorators, scrollables, animated - Update openUrl action description to reflect url_launcher integration - Expand accessibility section with new Semantics-wrapped components Made-with: Cursor --- docs/ARCHITECTURE.md | 84 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 52ae3e1..ef71978 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -120,9 +120,9 @@ flowchart LR --- -## Component Types +## Component Types (103 total) -### Layout Components (9) +### Core Layout Components (12) These components contain `children` and control layout. @@ -137,6 +137,9 @@ graph LR positioned["positioned"] wrap["wrap"] spacer["spacer"] + responsive["responsive"] + expanded["expanded"] + flexible["flexible"] style column fill:#E8EAF6 style row fill:#E8EAF6 style container fill:#E8EAF6 @@ -146,6 +149,9 @@ graph LR style positioned fill:#E8EAF6 style wrap fill:#E8EAF6 style spacer fill:#E8EAF6 + style responsive fill:#E8EAF6 + style expanded fill:#E8EAF6 + style flexible fill:#E8EAF6 ``` #### `column` / `row` @@ -208,6 +214,66 @@ graph LR | `height` | `number` | `16` | Vertical space | | `width` | `number` | none | Horizontal space | +### Layout Wrappers (22) + +Single-child wrappers that adjust positioning, sizing, and clipping. + +`center` · `align` · `padding` · `sizedBox` · `constrainedBox` · `fittedBox` · `fractionallySizedBox` · `intrinsicHeight` · `intrinsicWidth` · `limitedBox` · `overflowBox` · `aspectRatio` · `baseline` · `opacity` · `clipRRect` · `clipOval` · `safeArea` · `rotatedBox` · `ignorePointer` · `absorbPointer` · `offstage` · `visibility` + +### Decorators (7) + +Visual decoration and transformation wrappers. + +`material` · `hero` · `decoratedBox` · `indexedStack` · `transform` · `backdropFilter` · `banner` + +### Scrollables (6) + +Scrollable containers and sliver-based layouts. + +`scrollView` · `gridView` · `pageView` · `customScrollView` · `sliverList` · `sliverGrid` + +### Interactives (6) + +Gesture and interaction wrappers. + +`inkWell` · `gestureDetector` · `tooltip` · `dismissible` · `draggable` · `longPressDraggable` + +### Animated Widgets (9) + +Implicit animation wrappers driven by prop changes. + +`animatedContainer` · `animatedOpacity` · `animatedCrossFade` · `animatedSwitcher` · `animatedAlign` · `animatedPadding` · `animatedPositioned` · `animatedSize` · `animatedScale` + +### Tiles (5) + +Structured list item components. + +`listTile` · `expansionTile` · `switchListTile` · `checkboxListTile` · `radioListTile` + +### Tables (4) + +Tabular data layout components. + +`table` · `tableRow` · `tableCell` · `dataTable` + +### Text Variants (3) + +Advanced text rendering beyond the basic `text` component. + +`selectableText` · `richText` · `defaultTextStyle` + +### Button Variants (5) + +Additional button styles beyond the core `button`. + +`textButton` · `outlinedButton` · `iconButton` · `floatingActionButton` · `segmentedButton` + +### Miscellaneous (7) + +Utility and display widgets. + +`placeholder` · `circleAvatar` · `verticalDivider` · `popupMenuButton` · `searchBar` · `searchAnchor` · `tooltip` + ### Leaf Components (10) ```mermaid @@ -234,6 +300,12 @@ graph LR style switch_ fill:#C8E6C9 ``` +### Interactive Inputs (3) + +Input components with state management. + +`slider` · `rangeSlider` · `radio` + #### `text` | Prop | Type | Description | @@ -389,7 +461,7 @@ Copies the message to the system clipboard. ### `openUrl` -Signals the intent to open a URL (currently shows a snackbar). +Opens the URL in the device's default browser via `url_launcher`. ```json { "type": "openUrl", "message": "https://flutter.dev" } @@ -537,6 +609,12 @@ All interactive and leaf components include `Semantics` widgets for screen reade - `input` — marked as text field with label - `switch` — marked as toggled with label - `checkbox` — marked as checked with label +- `textButton` / `outlinedButton` — marked as button with label +- `inkWell` / `gestureDetector` — marked as button when action is present +- `selectableText` — labeled with content +- `richText` — labeled with concatenated span text +- `placeholder` — labeled as "Placeholder" +- `circleAvatar` — labeled with text or "Avatar" --- From bc170da62f4a73368ecb48f47d4a92dccec5eee9 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:50:24 -0300 Subject: [PATCH 21/28] docs: update README with 103 component types and new demo screen - Expand component table from 22 to 103 types across 13 categories - Add advanced_components to demo screens table - Add link to COMPONENTS.md reference catalog Made-with: Cursor --- README.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3352538..6c3b25a 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ graph TB end subgraph presentation ["lib/presentation"] pages["pages"] - widgets["widgets/ ×22"] + widgets["widgets/ ×103"] end subgraph playground ["lib/playground"] pg_page["PlaygroundPage"] @@ -101,13 +101,23 @@ graph TB ## Features -### Components (22 types) +### Components (103 types) | Category | Components | |----------|-----------| -| **Layout** | `column` · `row` · `container` · `card` · `listView` · `stack` · `positioned` · `wrap` · `spacer` · `responsive` · `expanded` · `flexible` | +| **Core Layout** | `column` · `row` · `container` · `card` · `listView` · `stack` · `positioned` · `wrap` · `spacer` · `responsive` · `expanded` · `flexible` | +| **Layout Wrappers** | `center` · `align` · `padding` · `sizedBox` · `constrainedBox` · `fittedBox` · `fractionallySizedBox` · `intrinsicHeight` · `intrinsicWidth` · `limitedBox` · `overflowBox` · `aspectRatio` · `baseline` · `opacity` · `clipRRect` · `clipOval` · `safeArea` · `rotatedBox` · `ignorePointer` · `absorbPointer` · `offstage` · `visibility` | +| **Decorators** | `material` · `hero` · `decoratedBox` · `indexedStack` · `transform` · `backdropFilter` · `banner` | +| **Scrollables** | `scrollView` · `gridView` · `pageView` · `customScrollView` · `sliverList` · `sliverGrid` | +| **Interactives** | `inkWell` · `gestureDetector` · `tooltip` · `dismissible` · `draggable` · `longPressDraggable` | +| **Animated** | `animatedContainer` · `animatedOpacity` · `animatedCrossFade` · `animatedSwitcher` · `animatedAlign` · `animatedPadding` · `animatedPositioned` · `animatedSize` · `animatedScale` | +| **Tiles** | `listTile` · `expansionTile` · `switchListTile` · `checkboxListTile` · `radioListTile` | +| **Tables** | `table` · `tableRow` · `tableCell` · `dataTable` | +| **Text Variants** | `selectableText` · `richText` · `defaultTextStyle` | +| **Button Variants** | `textButton` · `outlinedButton` · `iconButton` · `floatingActionButton` · `segmentedButton` | +| **Media & Display** | `placeholder` · `circleAvatar` · `verticalDivider` · `popupMenuButton` · `searchBar` · `searchAnchor` · `tooltip` | | **Leaf** | `text` · `button` · `image` · `input` · `divider` · `icon` · `chip` · `progress` · `badge` | -| **Interactive** | `switch` · `checkbox` · `dropdown` · `tabBar` · `carousel` | +| **Interactive Inputs** | `switch` · `checkbox` · `dropdown` · `tabBar` · `carousel` · `slider` · `rangeSlider` · `radio` | ### Actions (7 types) @@ -137,10 +147,11 @@ graph TB | `home` | Welcome page with navigation to all demos and a banner image | | `profile` | User profile with avatar, details card, and snackbar action | | `form` | Feedback form with validation, entrance animations, and submit | -| `components_showcase` | Every component type in one screen | +| `components_showcase` | Every core component type in one screen | | `expressions_demo` | Template interpolation and conditional visibility | | `theme_demo` | Dark theme applied via JSON contract | | `new_components` | Dropdown, tab bar, and carousel showcase | +| `advanced_components` | Layout wrappers, decorators, tiles, buttons, text variants, and misc widgets | --- @@ -245,6 +256,7 @@ _registry.register('yourType', buildYourComponent); ## Documentation - [Architecture & Schema Specification](docs/ARCHITECTURE.md) +- [Component Reference Catalog](docs/COMPONENTS.md) - [Mock Server](server/README.md) --- From 20a39cb32efa545dd051c5a7e6a6c4aabcd52f17 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:50:24 -0300 Subject: [PATCH 22/28] feat: add advanced components demo screen - Showcase layout wrappers, decorators, tiles, button variants - Demo opacity levels, transforms, rich text, and circle avatars Made-with: Cursor --- assets/screens/advanced_components.json | 270 ++++++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 assets/screens/advanced_components.json diff --git a/assets/screens/advanced_components.json b/assets/screens/advanced_components.json new file mode 100644 index 0000000..000115c --- /dev/null +++ b/assets/screens/advanced_components.json @@ -0,0 +1,270 @@ +{ + "schemaVersion": "1.0", + "screen": { + "id": "advanced_components", + "title": "Advanced Components", + "root": { + "type": "column", + "props": { + "crossAxisAlignment": "stretch", + "padding": { "top": 24, "bottom": 32, "left": 24, "right": 24 } + }, + "children": [ + { + "type": "text", + "props": { + "content": "Layout Wrappers & Decorators", + "style": { "fontSize": 24, "fontWeight": "bold" } + } + }, + { "type": "spacer", "props": { "height": 8 } }, + + { + "type": "text", + "props": { + "content": "Center + ClipRRect", + "style": { "fontSize": 16, "fontWeight": "w600" } + } + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "center", + "children": [ + { + "type": "clipRRect", + "props": { "borderRadius": 16 }, + "children": [ + { + "type": "container", + "props": { + "padding": 16, + "decoration": { + "color": "#E3F2FD", + "borderRadius": 16 + } + }, + "children": [ + { + "type": "text", + "props": { "content": "Clipped rounded content" } + } + ] + } + ] + } + ] + }, + + { "type": "divider", "props": { "height": 32 } }, + + { + "type": "text", + "props": { + "content": "Opacity", + "style": { "fontSize": 16, "fontWeight": "w600" } + } + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "row", + "props": { "mainAxisAlignment": "spaceEvenly" }, + "children": [ + { + "type": "opacity", + "props": { "opacity": 1.0 }, + "children": [ + { + "type": "chip", + "props": { "label": "100%", "backgroundColor": "#2196F3" } + } + ] + }, + { + "type": "opacity", + "props": { "opacity": 0.6 }, + "children": [ + { + "type": "chip", + "props": { "label": "60%", "backgroundColor": "#2196F3" } + } + ] + }, + { + "type": "opacity", + "props": { "opacity": 0.3 }, + "children": [ + { + "type": "chip", + "props": { "label": "30%", "backgroundColor": "#2196F3" } + } + ] + } + ] + }, + + { "type": "divider", "props": { "height": 32 } }, + + { + "type": "text", + "props": { + "content": "Transform (Rotate & Scale)", + "style": { "fontSize": 16, "fontWeight": "w600" } + } + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "row", + "props": { "mainAxisAlignment": "spaceEvenly" }, + "children": [ + { + "type": "transform", + "props": { "transformType": "rotate", "angle": 0.3 }, + "children": [ + { "type": "icon", "props": { "name": "star", "size": 40, "color": "#FF9800" } } + ] + }, + { + "type": "transform", + "props": { "transformType": "scale", "scale": 1.5 }, + "children": [ + { "type": "icon", "props": { "name": "favorite", "size": 32, "color": "#E91E63" } } + ] + } + ] + }, + + { "type": "divider", "props": { "height": 32 } }, + + { + "type": "text", + "props": { + "content": "Tiles", + "style": { "fontSize": 16, "fontWeight": "w600" } + } + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "listTile", + "props": { + "title": "Settings", + "subtitle": "Manage your preferences", + "leadingIcon": "settings" + } + }, + { + "type": "expansionTile", + "props": { "title": "More Options", "initiallyExpanded": false }, + "children": [ + { + "type": "padding", + "props": { "padding": 16 }, + "children": [ + { "type": "text", "props": { "content": "Hidden content revealed on expand." } } + ] + } + ] + }, + + { "type": "divider", "props": { "height": 32 } }, + + { + "type": "text", + "props": { + "content": "Button Variants", + "style": { "fontSize": 16, "fontWeight": "w600" } + } + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "row", + "props": { "mainAxisAlignment": "spaceEvenly" }, + "children": [ + { + "type": "textButton", + "props": { "label": "Text" }, + "action": { "type": "snackbar", "message": "Text button tapped" } + }, + { + "type": "outlinedButton", + "props": { "label": "Outlined" }, + "action": { "type": "snackbar", "message": "Outlined button tapped" } + }, + { + "type": "iconButton", + "props": { "icon": "thumb_up", "tooltip": "Like" }, + "action": { "type": "snackbar", "message": "Liked!" } + } + ] + }, + + { "type": "divider", "props": { "height": 32 } }, + + { + "type": "text", + "props": { + "content": "Text Variants", + "style": { "fontSize": 16, "fontWeight": "w600" } + } + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "selectableText", + "props": { + "content": "Long-press to select this text and copy it." + } + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "richText", + "props": { + "spans": [ + { "text": "Bold ", "fontWeight": "bold" }, + { "text": "and ", "color": "#666666" }, + { "text": "colored", "color": "#E91E63", "fontWeight": "w600" }, + { "text": " in one line." } + ] + } + }, + + { "type": "divider", "props": { "height": 32 } }, + + { + "type": "text", + "props": { + "content": "Miscellaneous", + "style": { "fontSize": 16, "fontWeight": "w600" } + } + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "row", + "props": { "mainAxisAlignment": "spaceEvenly" }, + "children": [ + { + "type": "circleAvatar", + "props": { "label": "AB", "radius": 28, "backgroundColor": "#7C4DFF" } + }, + { + "type": "circleAvatar", + "props": { "icon": "person", "radius": 28, "backgroundColor": "#00BCD4" } + }, + { + "type": "circleAvatar", + "props": { "label": "ZZ", "radius": 28, "backgroundColor": "#FF5722" } + } + ] + }, + { "type": "spacer", "props": { "height": 12 } }, + { + "type": "center", + "children": [ + { + "type": "placeholder", + "props": { "width": 200, "height": 80 } + } + ] + } + ] + } + } +} From b9833766097f9bcb9c8eb007df50553778be3636 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 13:50:24 -0300 Subject: [PATCH 23/28] feat: add navigation to advanced components from home screen - Add Advanced Components button between New Components and Expressions Demo Made-with: Cursor --- assets/screens/home.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/assets/screens/home.json b/assets/screens/home.json index be06b03..d57a715 100644 --- a/assets/screens/home.json +++ b/assets/screens/home.json @@ -82,6 +82,15 @@ "action": { "type": "navigate", "targetScreenId": "new_components" } }, { "type": "spacer", "props": { "height": 8 } }, + { + "type": "button", + "props": { + "label": "Advanced Components", + "style": { "backgroundColor": "#00897B", "textColor": "#FFFFFF" } + }, + "action": { "type": "navigate", "targetScreenId": "advanced_components" } + }, + { "type": "spacer", "props": { "height": 8 } }, { "type": "button", "props": { From 54f1115ac3d171ac45eef8369a7418be2344f693 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 15:09:13 -0300 Subject: [PATCH 24/28] feat: add onboarding welcome screen - Introduce the Getting Started guide entry point - List learning topics: JSON structure, components, actions, debugging - Navigation to onboarding_structure as next step Made-with: Cursor --- assets/screens/onboarding_welcome.json | 141 +++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 assets/screens/onboarding_welcome.json diff --git a/assets/screens/onboarding_welcome.json b/assets/screens/onboarding_welcome.json new file mode 100644 index 0000000..a23d9ca --- /dev/null +++ b/assets/screens/onboarding_welcome.json @@ -0,0 +1,141 @@ +{ + "schemaVersion": "1.0", + "screen": { + "id": "onboarding_welcome", + "title": "Getting Started", + "root": { + "type": "column", + "props": { + "crossAxisAlignment": "stretch", + "padding": { "top": 32, "bottom": 32, "left": 24, "right": 24 } + }, + "children": [ + { + "type": "center", + "children": [ + { + "type": "container", + "props": { + "padding": 20, + "decoration": { + "gradient": { + "type": "linear", + "colors": ["#820AD1", "#D72F87"], + "begin": "topLeft", + "end": "bottomRight" + }, + "borderRadius": 24 + } + }, + "children": [ + { "type": "icon", "props": { "name": "school", "size": 48, "color": "#FFFFFF" } } + ] + } + ] + }, + { "type": "spacer", "props": { "height": 24 } }, + { + "type": "text", + "props": { + "content": "Build Screens with JSON", + "style": { "fontSize": 26, "fontWeight": "bold", "textAlign": "center" } + } + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "text", + "props": { + "content": "Learn how to create Flutter interfaces dynamically using JSON contracts. No Dart code needed!", + "style": { "fontSize": 15, "color": "#666666", "textAlign": "center" } + } + }, + { "type": "divider", "props": { "height": 32 } }, + + { + "type": "text", + "props": { + "content": "What you'll learn", + "style": { "fontSize": 18, "fontWeight": "w700" } + } + }, + { "type": "spacer", "props": { "height": 12 } }, + + { + "type": "card", + "props": { "padding": 0, "elevation": 0 }, + "children": [ + { + "type": "column", + "children": [ + { + "type": "listTile", + "props": { + "title": "JSON Structure", + "subtitle": "Understand the contract schema", + "leadingIcon": "description" + } + }, + { "type": "divider", "props": { "height": 1, "indent": 56 } }, + { + "type": "listTile", + "props": { + "title": "Components", + "subtitle": "103 component types available", + "leadingIcon": "widgets" + } + }, + { "type": "divider", "props": { "height": 1, "indent": 56 } }, + { + "type": "listTile", + "props": { + "title": "Actions & Interactivity", + "subtitle": "Navigate, submit, and more", + "leadingIcon": "touch_app" + } + }, + { "type": "divider", "props": { "height": 1, "indent": 56 } }, + { + "type": "listTile", + "props": { + "title": "Expressions & Theming", + "subtitle": "Dynamic data and custom themes", + "leadingIcon": "palette" + } + }, + { "type": "divider", "props": { "height": 1, "indent": 56 } }, + { + "type": "listTile", + "props": { + "title": "Debugging Errors", + "subtitle": "Fix rendering issues quickly", + "leadingIcon": "bug_report" + } + } + ] + } + ] + }, + + { "type": "spacer", "props": { "height": 24 } }, + + { + "type": "button", + "props": { + "label": "Start Learning", + "style": { "backgroundColor": "#820AD1", "textColor": "#FFFFFF" } + }, + "action": { "type": "navigate", "targetScreenId": "onboarding_structure" } + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "button", + "props": { + "label": "Back to Home", + "style": { "backgroundColor": "#F5F0EB", "textColor": "#820AD1" } + }, + "action": { "type": "navigate", "targetScreenId": "home" } + } + ] + } + } +} From bf418bfb4ac107b46b55cd4939d5fee3ebc708e7 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 15:09:22 -0300 Subject: [PATCH 25/28] feat: add onboarding structure screen - Explain the four sections of a JSON contract: schemaVersion, screen, context, theme - Include tip box with component node anatomy Made-with: Cursor --- assets/screens/onboarding_structure.json | 181 +++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 assets/screens/onboarding_structure.json diff --git a/assets/screens/onboarding_structure.json b/assets/screens/onboarding_structure.json new file mode 100644 index 0000000..dde536d --- /dev/null +++ b/assets/screens/onboarding_structure.json @@ -0,0 +1,181 @@ +{ + "schemaVersion": "1.0", + "screen": { + "id": "onboarding_structure", + "title": "JSON Structure", + "root": { + "type": "column", + "props": { + "crossAxisAlignment": "stretch", + "padding": { "top": 24, "bottom": 32, "left": 24, "right": 24 } + }, + "children": [ + { + "type": "container", + "props": { + "padding": { "top": 4, "bottom": 4, "left": 12, "right": 12 }, + "decoration": { "color": "#E8F5E9", "borderRadius": 8 } + }, + "children": [ + { + "type": "text", + "props": { "content": "Step 1 of 4", "style": { "fontSize": 12, "fontWeight": "w600", "color": "#2E7D32" } } + } + ] + }, + { "type": "spacer", "props": { "height": 16 } }, + { + "type": "text", + "props": { + "content": "The JSON Contract", + "style": { "fontSize": 22, "fontWeight": "bold" } + } + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "text", + "props": { + "content": "Every screen is defined by a JSON contract with 4 main sections:", + "style": { "fontSize": 14, "color": "#666666" } + } + }, + { "type": "spacer", "props": { "height": 16 } }, + + { + "type": "card", + "props": { "padding": { "top": 16, "bottom": 16, "left": 16, "right": 16 }, "elevation": 0 }, + "children": [ + { + "type": "column", + "props": { "crossAxisAlignment": "start" }, + "children": [ + { + "type": "row", + "children": [ + { "type": "icon", "props": { "name": "tag", "size": 20, "color": "#820AD1" } }, + { "type": "spacer", "props": { "width": 8, "height": 1 } }, + { "type": "text", "props": { "content": "schemaVersion", "style": { "fontWeight": "w600", "fontSize": 15 } } } + ] + }, + { "type": "spacer", "props": { "height": 4 } }, + { "type": "text", "props": { "content": "Always \"1.0\" — tells the engine which format to expect.", "style": { "fontSize": 13, "color": "#666666" } } } + ] + } + ] + }, + { "type": "spacer", "props": { "height": 8 } }, + + { + "type": "card", + "props": { "padding": { "top": 16, "bottom": 16, "left": 16, "right": 16 }, "elevation": 0 }, + "children": [ + { + "type": "column", + "props": { "crossAxisAlignment": "start" }, + "children": [ + { + "type": "row", + "children": [ + { "type": "icon", "props": { "name": "phone_android", "size": 20, "color": "#D72F87" } }, + { "type": "spacer", "props": { "width": 8, "height": 1 } }, + { "type": "text", "props": { "content": "screen", "style": { "fontWeight": "w600", "fontSize": 15 } } } + ] + }, + { "type": "spacer", "props": { "height": 4 } }, + { "type": "text", "props": { "content": "Contains id, title, and root. The root is your component tree.", "style": { "fontSize": 13, "color": "#666666" } } } + ] + } + ] + }, + { "type": "spacer", "props": { "height": 8 } }, + + { + "type": "card", + "props": { "padding": { "top": 16, "bottom": 16, "left": 16, "right": 16 }, "elevation": 0 }, + "children": [ + { + "type": "column", + "props": { "crossAxisAlignment": "start" }, + "children": [ + { + "type": "row", + "children": [ + { "type": "icon", "props": { "name": "data_object", "size": 20, "color": "#FF9800" } }, + { "type": "spacer", "props": { "width": 8, "height": 1 } }, + { "type": "text", "props": { "content": "context (optional)", "style": { "fontWeight": "w600", "fontSize": 15 } } } + ] + }, + { "type": "spacer", "props": { "height": 4 } }, + { "type": "text", "props": { "content": "Variables for template interpolation. Use {{user.name}} in props.", "style": { "fontSize": 13, "color": "#666666" } } } + ] + } + ] + }, + { "type": "spacer", "props": { "height": 8 } }, + + { + "type": "card", + "props": { "padding": { "top": 16, "bottom": 16, "left": 16, "right": 16 }, "elevation": 0 }, + "children": [ + { + "type": "column", + "props": { "crossAxisAlignment": "start" }, + "children": [ + { + "type": "row", + "children": [ + { "type": "icon", "props": { "name": "palette", "size": 20, "color": "#00897B" } }, + { "type": "spacer", "props": { "width": 8, "height": 1 } }, + { "type": "text", "props": { "content": "theme (optional)", "style": { "fontWeight": "w600", "fontSize": 15 } } } + ] + }, + { "type": "spacer", "props": { "height": 4 } }, + { "type": "text", "props": { "content": "Per-screen colors, fonts, and brightness. Overrides the app theme.", "style": { "fontSize": 13, "color": "#666666" } } } + ] + } + ] + }, + + { "type": "divider", "props": { "height": 32 } }, + + { + "type": "container", + "props": { + "padding": 16, + "decoration": { "color": "#FFF8E1", "borderRadius": 12 } + }, + "children": [ + { + "type": "row", + "props": { "crossAxisAlignment": "start" }, + "children": [ + { "type": "icon", "props": { "name": "lightbulb", "size": 20, "color": "#F57F17" } }, + { "type": "spacer", "props": { "width": 10, "height": 1 } }, + { + "type": "expanded", + "children": [ + { + "type": "text", + "props": { + "content": "Tip: Every component node has a \"type\" (like \"text\", \"column\", \"button\") and \"props\" (the component's settings). Layout components also have \"children\".", + "style": { "fontSize": 13, "color": "#795548" } + } + } + ] + } + ] + } + ] + }, + + { "type": "spacer", "props": { "height": 24 } }, + + { + "type": "button", + "props": { "label": "Next: Components", "style": { "backgroundColor": "#820AD1", "textColor": "#FFFFFF" } }, + "action": { "type": "navigate", "targetScreenId": "onboarding_components" } + } + ] + } + } +} From 33ad5e6f0400f6c821177c3fa4690443511673dd Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 15:10:21 -0300 Subject: [PATCH 26/28] feat: add onboarding components screen - Differentiate layout vs leaf component types with examples - Show live JSON-to-widget rendering for column and text - Chip tags listing available component types Made-with: Cursor --- assets/screens/onboarding_components.json | 157 ++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 assets/screens/onboarding_components.json diff --git a/assets/screens/onboarding_components.json b/assets/screens/onboarding_components.json new file mode 100644 index 0000000..d0faab7 --- /dev/null +++ b/assets/screens/onboarding_components.json @@ -0,0 +1,157 @@ +{ + "schemaVersion": "1.0", + "screen": { + "id": "onboarding_components", + "title": "Components", + "root": { + "type": "column", + "props": { + "crossAxisAlignment": "stretch", + "padding": { "top": 24, "bottom": 32, "left": 24, "right": 24 } + }, + "children": [ + { + "type": "container", + "props": { + "padding": { "top": 4, "bottom": 4, "left": 12, "right": 12 }, + "decoration": { "color": "#E3F2FD", "borderRadius": 8 } + }, + "children": [ + { "type": "text", "props": { "content": "Step 2 of 4", "style": { "fontSize": 12, "fontWeight": "w600", "color": "#1565C0" } } } + ] + }, + { "type": "spacer", "props": { "height": 16 } }, + { + "type": "text", + "props": { "content": "Component Types", "style": { "fontSize": 22, "fontWeight": "bold" } } + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "text", + "props": { "content": "Components are divided into two categories: layout (containers with children) and leaf (visual elements).", "style": { "fontSize": 14, "color": "#666666" } } + }, + { "type": "divider", "props": { "height": 24 } }, + + { + "type": "text", + "props": { "content": "Layout Components", "style": { "fontSize": 16, "fontWeight": "w700" } } + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "text", + "props": { "content": "These contain children and control how they are arranged.", "style": { "fontSize": 13, "color": "#888888" } } + }, + { "type": "spacer", "props": { "height": 12 } }, + + { + "type": "container", + "props": { "padding": 12, "decoration": { "color": "#F3E5F5", "borderRadius": 12 } }, + "children": [ + { + "type": "column", + "props": { "crossAxisAlignment": "start" }, + "children": [ + { "type": "text", "props": { "content": "Example: Column layout", "style": { "fontSize": 12, "fontWeight": "w600", "color": "#820AD1" } } }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "container", + "props": { "padding": 10, "decoration": { "color": "#FFFFFF", "borderRadius": 8 } }, + "children": [ + { + "type": "text", + "props": { "content": "{ \"type\": \"column\",\n \"props\": { \"crossAxisAlignment\": \"stretch\" },\n \"children\": [ ... ] }", "style": { "fontSize": 12, "color": "#333333" } } + } + ] + } + ] + } + ] + }, + { "type": "spacer", "props": { "height": 12 } }, + + { + "type": "wrap", + "props": { "spacing": 6, "runSpacing": 6 }, + "children": [ + { "type": "chip", "props": { "label": "column", "backgroundColor": "#E8EAF6" } }, + { "type": "chip", "props": { "label": "row", "backgroundColor": "#E8EAF6" } }, + { "type": "chip", "props": { "label": "stack", "backgroundColor": "#E8EAF6" } }, + { "type": "chip", "props": { "label": "container", "backgroundColor": "#E8EAF6" } }, + { "type": "chip", "props": { "label": "card", "backgroundColor": "#E8EAF6" } }, + { "type": "chip", "props": { "label": "listView", "backgroundColor": "#E8EAF6" } }, + { "type": "chip", "props": { "label": "wrap", "backgroundColor": "#E8EAF6" } }, + { "type": "chip", "props": { "label": "center", "backgroundColor": "#E8EAF6" } }, + { "type": "chip", "props": { "label": "padding", "backgroundColor": "#E8EAF6" } }, + { "type": "chip", "props": { "label": "+90 more...", "backgroundColor": "#E0E0E0" } } + ] + }, + + { "type": "divider", "props": { "height": 24 } }, + + { + "type": "text", + "props": { "content": "Leaf Components", "style": { "fontSize": 16, "fontWeight": "w700" } } + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "text", + "props": { "content": "These render directly and don't have children.", "style": { "fontSize": 13, "color": "#888888" } } + }, + { "type": "spacer", "props": { "height": 12 } }, + + { + "type": "container", + "props": { "padding": 12, "decoration": { "color": "#E8F5E9", "borderRadius": 12 } }, + "children": [ + { + "type": "column", + "props": { "crossAxisAlignment": "start" }, + "children": [ + { "type": "text", "props": { "content": "Example: Text component", "style": { "fontSize": 12, "fontWeight": "w600", "color": "#2E7D32" } } }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "container", + "props": { "padding": 10, "decoration": { "color": "#FFFFFF", "borderRadius": 8 } }, + "children": [ + { + "type": "text", + "props": { "content": "{ \"type\": \"text\",\n \"props\": {\n \"content\": \"Hello World\",\n \"style\": { \"fontSize\": 24 }\n } }", "style": { "fontSize": 12, "color": "#333333" } } + } + ] + }, + { "type": "spacer", "props": { "height": 8 } }, + { "type": "text", "props": { "content": "Result:", "style": { "fontSize": 12, "fontWeight": "w600", "color": "#2E7D32" } } }, + { "type": "spacer", "props": { "height": 4 } }, + { "type": "text", "props": { "content": "Hello World", "style": { "fontSize": 24 } } } + ] + } + ] + }, + { "type": "spacer", "props": { "height": 12 } }, + + { + "type": "wrap", + "props": { "spacing": 6, "runSpacing": 6 }, + "children": [ + { "type": "chip", "props": { "label": "text", "backgroundColor": "#C8E6C9" } }, + { "type": "chip", "props": { "label": "button", "backgroundColor": "#C8E6C9" } }, + { "type": "chip", "props": { "label": "image", "backgroundColor": "#C8E6C9" } }, + { "type": "chip", "props": { "label": "input", "backgroundColor": "#C8E6C9" } }, + { "type": "chip", "props": { "label": "icon", "backgroundColor": "#C8E6C9" } }, + { "type": "chip", "props": { "label": "divider", "backgroundColor": "#C8E6C9" } }, + { "type": "chip", "props": { "label": "slider", "backgroundColor": "#C8E6C9" } }, + { "type": "chip", "props": { "label": "switch", "backgroundColor": "#C8E6C9" } } + ] + }, + + { "type": "spacer", "props": { "height": 24 } }, + + { + "type": "button", + "props": { "label": "Next: Actions & Interactivity", "style": { "backgroundColor": "#820AD1", "textColor": "#FFFFFF" } }, + "action": { "type": "navigate", "targetScreenId": "onboarding_actions" } + } + ] + } + } +} From b8a2b8ecf1995b22ea28cb424789c75ad6665bc8 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 15:10:38 -0300 Subject: [PATCH 27/28] feat: add onboarding actions screen - Interactive demos for navigate, snackbar, copyToClipboard, showDialog - Each action has a 'Try it' button for hands-on testing - Proper expanded layout for row-based card content Made-with: Cursor --- assets/screens/onboarding_actions.json | 200 +++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 assets/screens/onboarding_actions.json diff --git a/assets/screens/onboarding_actions.json b/assets/screens/onboarding_actions.json new file mode 100644 index 0000000..a4c043e --- /dev/null +++ b/assets/screens/onboarding_actions.json @@ -0,0 +1,200 @@ +{ + "schemaVersion": "1.0", + "screen": { + "id": "onboarding_actions", + "title": "Actions", + "root": { + "type": "column", + "props": { + "crossAxisAlignment": "stretch", + "padding": { "top": 24, "bottom": 32, "left": 24, "right": 24 } + }, + "children": [ + { + "type": "container", + "props": { + "padding": { "top": 4, "bottom": 4, "left": 12, "right": 12 }, + "decoration": { "color": "#FFF3E0", "borderRadius": 8 } + }, + "children": [ + { "type": "text", "props": { "content": "Step 3 of 4", "style": { "fontSize": 12, "fontWeight": "w600", "color": "#E65100" } } } + ] + }, + { "type": "spacer", "props": { "height": 16 } }, + { + "type": "text", + "props": { "content": "Actions & Interactivity", "style": { "fontSize": 22, "fontWeight": "bold" } } + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "text", + "props": { "content": "Any component can have an \"action\" field that defines what happens when the user interacts with it.", "style": { "fontSize": 14, "color": "#666666" } } + }, + { "type": "divider", "props": { "height": 24 } }, + + { + "type": "text", + "props": { "content": "Try these actions:", "style": { "fontSize": 16, "fontWeight": "w700" } } + }, + { "type": "spacer", "props": { "height": 12 } }, + + { + "type": "card", + "props": { "padding": { "top": 12, "bottom": 12, "left": 16, "right": 16 }, "elevation": 0 }, + "children": [ + { + "type": "row", + "children": [ + { + "type": "expanded", + "children": [ + { + "type": "column", + "props": { "crossAxisAlignment": "start" }, + "children": [ + { "type": "text", "props": { "content": "navigate", "style": { "fontWeight": "w600" } } }, + { "type": "text", "props": { "content": "Go to another screen", "style": { "fontSize": 12, "color": "#888888" } } } + ] + } + ] + }, + { + "type": "button", + "props": { "label": "Try it", "style": { "backgroundColor": "#820AD1", "textColor": "#FFFFFF" } }, + "action": { "type": "navigate", "targetScreenId": "profile" } + } + ] + } + ] + }, + { "type": "spacer", "props": { "height": 8 } }, + + { + "type": "card", + "props": { "padding": { "top": 12, "bottom": 12, "left": 16, "right": 16 }, "elevation": 0 }, + "children": [ + { + "type": "row", + "children": [ + { + "type": "expanded", + "children": [ + { + "type": "column", + "props": { "crossAxisAlignment": "start" }, + "children": [ + { "type": "text", "props": { "content": "snackbar", "style": { "fontWeight": "w600" } } }, + { "type": "text", "props": { "content": "Show a message", "style": { "fontSize": 12, "color": "#888888" } } } + ] + } + ] + }, + { + "type": "button", + "props": { "label": "Try it", "style": { "backgroundColor": "#D72F87", "textColor": "#FFFFFF" } }, + "action": { "type": "snackbar", "message": "This is a snackbar from JSON!" } + } + ] + } + ] + }, + { "type": "spacer", "props": { "height": 8 } }, + + { + "type": "card", + "props": { "padding": { "top": 12, "bottom": 12, "left": 16, "right": 16 }, "elevation": 0 }, + "children": [ + { + "type": "row", + "children": [ + { + "type": "expanded", + "children": [ + { + "type": "column", + "props": { "crossAxisAlignment": "start" }, + "children": [ + { "type": "text", "props": { "content": "copyToClipboard", "style": { "fontWeight": "w600" } } }, + { "type": "text", "props": { "content": "Copy text", "style": { "fontSize": 12, "color": "#888888" } } } + ] + } + ] + }, + { + "type": "button", + "props": { "label": "Try it", "style": { "backgroundColor": "#00897B", "textColor": "#FFFFFF" } }, + "action": { "type": "copyToClipboard", "message": "Copied from Server-Driven UI!" } + } + ] + } + ] + }, + { "type": "spacer", "props": { "height": 8 } }, + + { + "type": "card", + "props": { "padding": { "top": 12, "bottom": 12, "left": 16, "right": 16 }, "elevation": 0 }, + "children": [ + { + "type": "row", + "children": [ + { + "type": "expanded", + "children": [ + { + "type": "column", + "props": { "crossAxisAlignment": "start" }, + "children": [ + { "type": "text", "props": { "content": "showDialog", "style": { "fontWeight": "w600" } } }, + { "type": "text", "props": { "content": "Show an alert", "style": { "fontSize": 12, "color": "#888888" } } } + ] + } + ] + }, + { + "type": "button", + "props": { "label": "Try it", "style": { "backgroundColor": "#FF5722", "textColor": "#FFFFFF" } }, + "action": { "type": "showDialog", "targetScreenId": "Well done!", "message": "You just triggered a dialog action from JSON." } + } + ] + } + ] + }, + + { "type": "divider", "props": { "height": 24 } }, + + { + "type": "container", + "props": { "padding": 16, "decoration": { "color": "#FFF8E1", "borderRadius": 12 } }, + "children": [ + { + "type": "row", + "props": { "crossAxisAlignment": "start" }, + "children": [ + { "type": "icon", "props": { "name": "lightbulb", "size": 20, "color": "#F57F17" } }, + { "type": "spacer", "props": { "width": 10, "height": 1 } }, + { + "type": "expanded", + "children": [ + { + "type": "text", + "props": { "content": "Tip: Actions are defined as { \"type\": \"snackbar\", \"message\": \"Hello!\" }. Add an \"action\" field to any component to make it interactive.", "style": { "fontSize": 13, "color": "#795548" } } + } + ] + } + ] + } + ] + }, + + { "type": "spacer", "props": { "height": 24 } }, + + { + "type": "button", + "props": { "label": "Next: Debugging", "style": { "backgroundColor": "#820AD1", "textColor": "#FFFFFF" } }, + "action": { "type": "navigate", "targetScreenId": "onboarding_debugging" } + } + ] + } + } +} From cf2347d1a2db003e3280342a60745ddf1700415f Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 1 Apr 2026 15:10:46 -0300 Subject: [PATCH 28/28] feat: add onboarding debugging screen - Document common mistakes: unknown type, missing props, invalid JSON - Highlight built-in safety features: Error Boundary and Contract Validator - Graduation section with link to Playground Made-with: Cursor --- assets/screens/onboarding_debugging.json | 230 +++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 assets/screens/onboarding_debugging.json diff --git a/assets/screens/onboarding_debugging.json b/assets/screens/onboarding_debugging.json new file mode 100644 index 0000000..8731539 --- /dev/null +++ b/assets/screens/onboarding_debugging.json @@ -0,0 +1,230 @@ +{ + "schemaVersion": "1.0", + "screen": { + "id": "onboarding_debugging", + "title": "Debugging", + "root": { + "type": "column", + "props": { + "crossAxisAlignment": "stretch", + "padding": { "top": 24, "bottom": 32, "left": 24, "right": 24 } + }, + "children": [ + { + "type": "container", + "props": { + "padding": { "top": 4, "bottom": 4, "left": 12, "right": 12 }, + "decoration": { "color": "#FFEBEE", "borderRadius": 8 } + }, + "children": [ + { "type": "text", "props": { "content": "Step 4 of 4", "style": { "fontSize": 12, "fontWeight": "w600", "color": "#C62828" } } } + ] + }, + { "type": "spacer", "props": { "height": 16 } }, + { + "type": "text", + "props": { "content": "Debugging & Errors", "style": { "fontSize": 22, "fontWeight": "bold" } } + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "text", + "props": { "content": "When something goes wrong in your JSON contract, the engine provides clear feedback.", "style": { "fontSize": 14, "color": "#666666" } } + }, + { "type": "divider", "props": { "height": 24 } }, + + { + "type": "text", + "props": { "content": "Common Mistakes", "style": { "fontSize": 16, "fontWeight": "w700" } } + }, + { "type": "spacer", "props": { "height": 12 } }, + + { + "type": "card", + "props": { "padding": { "top": 16, "bottom": 16, "left": 16, "right": 16 }, "elevation": 0 }, + "children": [ + { + "type": "column", + "props": { "crossAxisAlignment": "start" }, + "children": [ + { + "type": "row", + "children": [ + { + "type": "container", + "props": { "padding": 6, "decoration": { "color": "#FFEBEE", "borderRadius": 8 } }, + "children": [ + { "type": "icon", "props": { "name": "error_outline", "size": 18, "color": "#C62828" } } + ] + }, + { "type": "spacer", "props": { "width": 12, "height": 1 } }, + { "type": "text", "props": { "content": "Unknown component type", "style": { "fontWeight": "w600" } } } + ] + }, + { "type": "spacer", "props": { "height": 8 } }, + { "type": "text", "props": { "content": "Using a \"type\" that doesn't exist (e.g., \"textt\" instead of \"text\"). The Error Boundary will catch it and show a red fallback.", "style": { "fontSize": 13, "color": "#666666" } } } + ] + } + ] + }, + { "type": "spacer", "props": { "height": 8 } }, + + { + "type": "card", + "props": { "padding": { "top": 16, "bottom": 16, "left": 16, "right": 16 }, "elevation": 0 }, + "children": [ + { + "type": "column", + "props": { "crossAxisAlignment": "start" }, + "children": [ + { + "type": "row", + "children": [ + { + "type": "container", + "props": { "padding": 6, "decoration": { "color": "#FFF3E0", "borderRadius": 8 } }, + "children": [ + { "type": "icon", "props": { "name": "warning_amber", "size": 18, "color": "#E65100" } } + ] + }, + { "type": "spacer", "props": { "width": 12, "height": 1 } }, + { "type": "text", "props": { "content": "Missing required props", "style": { "fontWeight": "w600" } } } + ] + }, + { "type": "spacer", "props": { "height": 8 } }, + { "type": "text", "props": { "content": "Forgetting \"content\" on text, \"label\" on button, or \"url\" on image. The ContractValidator warns about these.", "style": { "fontSize": 13, "color": "#666666" } } } + ] + } + ] + }, + { "type": "spacer", "props": { "height": 8 } }, + + { + "type": "card", + "props": { "padding": { "top": 16, "bottom": 16, "left": 16, "right": 16 }, "elevation": 0 }, + "children": [ + { + "type": "column", + "props": { "crossAxisAlignment": "start" }, + "children": [ + { + "type": "row", + "children": [ + { + "type": "container", + "props": { "padding": 6, "decoration": { "color": "#E3F2FD", "borderRadius": 8 } }, + "children": [ + { "type": "icon", "props": { "name": "info_outline", "size": 18, "color": "#1565C0" } } + ] + }, + { "type": "spacer", "props": { "width": 12, "height": 1 } }, + { "type": "text", "props": { "content": "Invalid JSON syntax", "style": { "fontWeight": "w600" } } } + ] + }, + { "type": "spacer", "props": { "height": 8 } }, + { "type": "text", "props": { "content": "Missing commas, brackets, or quotes. Use the Playground to live-edit and catch syntax errors instantly.", "style": { "fontSize": 13, "color": "#666666" } } } + ] + } + ] + }, + + { "type": "divider", "props": { "height": 24 } }, + + { + "type": "text", + "props": { "content": "Built-in Safety", "style": { "fontSize": 16, "fontWeight": "w700" } } + }, + { "type": "spacer", "props": { "height": 12 } }, + + { + "type": "container", + "props": { "padding": 16, "decoration": { "color": "#E8F5E9", "borderRadius": 12 } }, + "children": [ + { + "type": "column", + "props": { "crossAxisAlignment": "start" }, + "children": [ + { + "type": "row", + "children": [ + { "type": "icon", "props": { "name": "shield", "size": 20, "color": "#2E7D32" } }, + { "type": "spacer", "props": { "width": 8, "height": 1 } }, + { "type": "text", "props": { "content": "Error Boundary", "style": { "fontWeight": "w600", "color": "#2E7D32" } } } + ] + }, + { "type": "spacer", "props": { "height": 6 } }, + { "type": "text", "props": { "content": "Each component is wrapped in an error boundary. If one fails, only that component shows an error — the rest of the screen continues working.", "style": { "fontSize": 13, "color": "#555555" } } } + ] + } + ] + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "container", + "props": { "padding": 16, "decoration": { "color": "#E8F5E9", "borderRadius": 12 } }, + "children": [ + { + "type": "column", + "props": { "crossAxisAlignment": "start" }, + "children": [ + { + "type": "row", + "children": [ + { "type": "icon", "props": { "name": "checklist", "size": 20, "color": "#2E7D32" } }, + { "type": "spacer", "props": { "width": 8, "height": 1 } }, + { "type": "text", "props": { "content": "Contract Validator", "style": { "fontWeight": "w600", "color": "#2E7D32" } } } + ] + }, + { "type": "spacer", "props": { "height": 6 } }, + { "type": "text", "props": { "content": "Before rendering, the engine validates your contract and warns about issues: unknown types, missing props, invalid actions.", "style": { "fontSize": 13, "color": "#555555" } } } + ] + } + ] + }, + + { "type": "spacer", "props": { "height": 24 } }, + + { + "type": "container", + "props": { + "padding": 20, + "decoration": { + "gradient": { + "type": "linear", + "colors": ["#820AD1", "#D72F87"], + "begin": "topLeft", + "end": "bottomRight" + }, + "borderRadius": 16 + } + }, + "children": [ + { + "type": "column", + "props": { "crossAxisAlignment": "center" }, + "children": [ + { "type": "icon", "props": { "name": "celebration", "size": 40, "color": "#FFFFFF" } }, + { "type": "spacer", "props": { "height": 12 } }, + { "type": "text", "props": { "content": "You're ready!", "style": { "fontSize": 20, "fontWeight": "bold", "color": "#FFFFFF", "textAlign": "center" } } }, + { "type": "spacer", "props": { "height": 4 } }, + { "type": "text", "props": { "content": "Head to the Playground to start building your own screens.", "style": { "fontSize": 14, "color": "#CCFFFFFF", "textAlign": "center" } } } + ] + } + ] + }, + { "type": "spacer", "props": { "height": 16 } }, + + { + "type": "button", + "props": { "label": "Open Playground", "style": { "backgroundColor": "#820AD1", "textColor": "#FFFFFF" } }, + "action": { "type": "navigate", "targetScreenId": "home" } + }, + { "type": "spacer", "props": { "height": 8 } }, + { + "type": "button", + "props": { "label": "Back to Home", "style": { "backgroundColor": "#F5F0EB", "textColor": "#820AD1" } }, + "action": { "type": "navigate", "targetScreenId": "home" } + } + ] + } + } +}