From e6370d6416312fdf2aaa8ba6d5053c78e8c0770b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 19:43:45 +0000 Subject: [PATCH] codelessly_sdk 7.1.1: size childless badges to their content - A badge with no child sat in an Align that expanded to loose constraints, so the editor measured a wrap-content badge as the whole viewport. Give the Align width and height factors of 1: it still positions the badge inside a fixed-size node box. - Wrap chips in a transparent Material, so they render in hosts that provide no Material ancestor instead of throwing. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01A67XoQYsLVvoMtiKcXXUxY --- CHANGELOG.md | 6 ++ .../passive_badge_transformer.dart | 10 ++- .../passive_chip_transformer.dart | 7 +- pubspec.yaml | 2 +- test/smoke_test.dart | 75 +++++++++++++++++++ 5 files changed, 96 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd6fa34..8f4c05d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.1.1 + +- A badge with no child no longer fills loose constraints, so a badge set + to wrap its content sizes to the badge. +- Chips no longer throw when no `Material` ancestor is present. + ## 7.1.0 - Require codelessly_api 5.1.0. diff --git a/lib/src/transformers/node_transformers/passive_badge_transformer.dart b/lib/src/transformers/node_transformers/passive_badge_transformer.dart index 1098f39..dc0262a 100644 --- a/lib/src/transformers/node_transformers/passive_badge_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_badge_transformer.dart @@ -107,9 +107,15 @@ class PassiveBadgeWidget extends StatelessWidget { return AdaptiveNodeBox( node: node, // Without a child, the badge keeps its own size and sits at its - // alignment inside the node's box. + // alignment inside the node's box. The size factors keep it from + // filling loose constraints when the node wraps its content. child: childNode == null - ? Align(alignment: alignment, child: badge) + ? Align( + alignment: alignment, + widthFactor: 1, + heightFactor: 1, + child: badge, + ) : badge, ); } diff --git a/lib/src/transformers/node_transformers/passive_chip_transformer.dart b/lib/src/transformers/node_transformers/passive_chip_transformer.dart index d713856..5bbefd0 100644 --- a/lib/src/transformers/node_transformers/passive_chip_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_chip_transformer.dart @@ -310,6 +310,11 @@ class TransformerChip extends StatelessWidget { ), }; - return AdaptiveNodeBox(node: node, child: chip); + // Chips draw ink and need a Material ancestor, which a layout may not + // provide (e.g. when measured offscreen). + return AdaptiveNodeBox( + node: node, + child: Material(type: MaterialType.transparency, child: chip), + ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 302d80c..f2ef59a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: codelessly_sdk description: Codelessly CloudUIâ„¢ - Server-driven UI for Flutter. Supercharge your Flutter apps with dynamic UI and real-time updates. Build and publish UI without code! -version: 7.1.0 +version: 7.1.1 homepage: https://codelessly.com/ repository: https://github.com/Codelessly/CodelesslySDK issue_tracker: https://github.com/Codelessly/CodelesslySDK/issues diff --git a/test/smoke_test.dart b/test/smoke_test.dart index b11e0a7..99d8870 100644 --- a/test/smoke_test.dart +++ b/test/smoke_test.dart @@ -362,4 +362,79 @@ void main() { expect(PassiveSegmentedButtonWidget.parseSelection('[]'), isEmpty); expect(PassiveSegmentedButtonWidget.parseSelection('a'), {'a'}); }); + + // The editor measures wrap-content nodes offscreen, under loose constraints + // and without a Material ancestor. + testWidgets('badge, chip and segmented button wrap their content', ( + tester, + ) async { + const measure = WidgetBuildSettings( + debugLabel: 'test', + withMargins: false, + withConstraints: false, + withAlignment: false, + isPreview: true, + ); + final nodes = [ + BadgeNode( + id: 'badge', + name: 'Badge', + basicBoxLocal: NodeBox(0, 0, 48, 48), + horizontalFit: SizeFit.shrinkWrap, + verticalFit: SizeFit.shrinkWrap, + properties: BadgeProperties(label: '3'), + ), + for (final type in ChipType.values) + ChipNode( + id: 'chip-${type.name}', + name: 'Chip', + basicBoxLocal: NodeBox(0, 0, 100, 32), + horizontalFit: SizeFit.shrinkWrap, + verticalFit: SizeFit.shrinkWrap, + properties: ChipProperties(chipType: type, label: 'Label'), + ), + SegmentedButtonNode( + id: 'segmented', + name: 'Segmented', + basicBoxLocal: NodeBox(0, 0, 200, 40), + horizontalFit: SizeFit.shrinkWrap, + verticalFit: SizeFit.shrinkWrap, + properties: SegmentedButtonProperties( + segments: const [ + ButtonSegmentModel(value: 'a', label: 'A'), + ButtonSegmentModel(value: 'b', label: 'B'), + ], + selected: const ['a'], + ), + ), + ]; + final manager = managerFor(nodes); + for (final node in nodes) { + final key = GlobalKey(); + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: CodelesslyContext.empty(), + // MaterialApp without a Scaffold: localizations but no Material. + child: MaterialApp( + home: Center( + child: Builder( + builder: (context) => KeyedSubtree( + key: key, + child: manager.buildWidgetByID( + node.id, + context, + settings: measure, + ), + ), + ), + ), + ), + ), + ); + expect(tester.takeException(), isNull, reason: node.id); + final size = tester.getSize(find.byKey(key)); + expect(size.width, lessThan(300), reason: '${node.id} $size'); + expect(size.height, lessThan(100), reason: '${node.id} $size'); + } + }); }