From d08841657854fc46cabb6585ca67699334d40a01 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 22:26:19 +0000 Subject: [PATCH 01/13] Paint: repeating gradients with tileMode and gradientExtent A gradient paint can now repeat or mirror past its ends (tileMode, default clamp and left out of the JSON) over a set length in pixels (gradientExtent), so a short gradient with hard stops paints stripes. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 9 +++++ README.md | 1 + lib/src/api/models/paint.dart | 58 ++++++++++++++++++++++++++++ lib/src/api/models/paint.g.dart | 18 +++++++++ test/gradient_tile_mode_test.dart | 63 +++++++++++++++++++++++++++++++ 5 files changed, 149 insertions(+) create mode 100644 test/gradient_tile_mode_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cb1e325..7c94ed45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## Unreleased + +### New + +- Repeating gradients: `PaintModel.tileMode` (`GradientTileMode` clamp, + repeated or mirror; default clamp, left out of the JSON) and + `PaintModel.gradientExtent`, the length in pixels of one run of the + gradient. A short repeating gradient with hard stops paints stripes. + ## 6.1.0 ### States and conditions diff --git a/README.md b/README.md index 3dfa1cb6..e57967d5 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ or image texture. - `PaintModel`: Defines the style properties such as the type of paint (`type`), its visibility (`visible`), and its opacity (`opacity`), among others. Depending on the type of paint, it might have properties like color (`color`), gradient transform (`gradientTransform`), gradient stops (`gradientStops`), or image transform (`imageTransform`). + A gradient can repeat past its ends (`tileMode`) over a set length in pixels (`gradientExtent`), which makes stripes. ### Flutter parity diff --git a/lib/src/api/models/paint.dart b/lib/src/api/models/paint.dart index 85644755..7bff7843 100644 --- a/lib/src/api/models/paint.dart +++ b/lib/src/api/models/paint.dart @@ -29,6 +29,25 @@ enum ImageRepeatEnum { final String label; } +/// Describes how a gradient paints past its ends, for example to repeat a +/// short gradient into stripes. Directly translates to Flutter's [TileMode]. +enum GradientTileMode { + /// The colors at the ends of the gradient extend past them. + clamp('Clamp'), + + /// The gradient repeats from its start after its end. + repeated('Repeat'), + + /// The gradient repeats, flipping direction on every repetition. + mirror('Mirror'); + + /// Default constructor for [GradientTileMode]. + const GradientTileMode(this.label); + + /// Displayable string representation of [GradientTileMode]. + final String label; +} + /// Generates a random id if the given [json] does not contain a value for the /// given [key]. Required for backwards compatibility. Object? _readId(Map json, String key) => json[key] ?? generateId(); @@ -88,6 +107,20 @@ class PaintModel with Equatable, SerializableMixin, FieldsHolder { /// neighboring gradient stops. final List? gradientStops; + /// How the gradient paints past its ends. + @JsonKey(unknownEnumValue: GradientTileMode.clamp) + final GradientTileMode tileMode; + + /// Length in logical pixels of one run of the gradient, from its first stop + /// to its last, along the gradient's axis. For a radial gradient it is the + /// radius. Null spans the gradient across the node's box. + /// + /// Combined with a repeating [tileMode], a short gradient tiles: stops + /// `[yellow 0, yellow 0.5, black 0.5, black 1]` with an extent of 20 paint + /// 10px yellow and black stripes. Two stops at one position give a hard + /// edge. + final double? gradientExtent; + // For image paint: /// Affine transform applied to the image. @@ -254,6 +287,8 @@ class PaintModel with Equatable, SerializableMixin, FieldsHolder { imageHash = null, gradientTransform = null, gradientStops = null, + tileMode = GradientTileMode.clamp, + gradientExtent = null, imageTransform = null, imageName = null, fit = Fit.none, @@ -293,6 +328,8 @@ class PaintModel with Equatable, SerializableMixin, FieldsHolder { type = PaintType.image, gradientTransform = null, gradientStops = null, + tileMode = GradientTileMode.clamp, + gradientExtent = null, color = null; /// Creates [PaintModel] with linear gradient. @@ -302,6 +339,8 @@ class PaintModel with Equatable, SerializableMixin, FieldsHolder { this.opacity = 1, this.gradientTransform, this.gradientStops, + this.tileMode = GradientTileMode.clamp, + this.gradientExtent, this.blendMode = BlendModeC.srcOver, }) : type = PaintType.gradientLinear, color = null, @@ -328,6 +367,8 @@ class PaintModel with Equatable, SerializableMixin, FieldsHolder { this.opacity = 1, this.gradientTransform, this.gradientStops, + this.tileMode = GradientTileMode.clamp, + this.gradientExtent, this.blendMode = BlendModeC.srcOver, }) : type = PaintType.gradientRadial, color = null, @@ -354,6 +395,8 @@ class PaintModel with Equatable, SerializableMixin, FieldsHolder { this.opacity = 1, this.gradientTransform, this.gradientStops, + this.tileMode = GradientTileMode.clamp, + this.gradientExtent, this.blendMode = BlendModeC.srcOver, }) : type = PaintType.gradientAngular, color = null, @@ -383,6 +426,8 @@ class PaintModel with Equatable, SerializableMixin, FieldsHolder { this.blendMode = BlendModeC.srcOver, this.gradientTransform, this.gradientStops, + this.tileMode = GradientTileMode.clamp, + this.gradientExtent, this.imageTransform, this.hasImageBytes = false, this.downloadUrl, @@ -409,6 +454,9 @@ class PaintModel with Equatable, SerializableMixin, FieldsHolder { BlendModeC? blendMode, List? gradientTransform, List? gradientStops, + GradientTileMode? tileMode, + double? gradientExtent, + bool clearGradientExtent = false, List>? imageTransform, double? scalingFactor, String? downloadUrl, @@ -437,6 +485,10 @@ class PaintModel with Equatable, SerializableMixin, FieldsHolder { blendMode: blendMode ?? this.blendMode, gradientTransform: gradientTransform ?? this.gradientTransform, gradientStops: gradientStops ?? this.gradientStops, + tileMode: tileMode ?? this.tileMode, + gradientExtent: clearGradientExtent + ? null + : gradientExtent ?? this.gradientExtent, imageTransform: imageTransform ?? this.imageTransform, hasImageBytes: hasImageBytes ?? this.hasImageBytes, downloadUrl: downloadUrl ?? this.downloadUrl, @@ -487,6 +539,8 @@ class PaintModel with Equatable, SerializableMixin, FieldsHolder { visible: visible, gradientStops: gradientStops, gradientTransform: gradientTransform, + tileMode: tileMode, + gradientExtent: gradientExtent, ); case PaintType.image: return PaintModel.image( @@ -526,6 +580,8 @@ class PaintModel with Equatable, SerializableMixin, FieldsHolder { if (blendMode != other.blendMode) return false; if (gradientTransform != other.gradientTransform) return false; if (gradientStops != other.gradientStops) return false; + if (tileMode != other.tileMode) return false; + if (gradientExtent != other.gradientExtent) return false; if (imageTransform != other.imageTransform) return false; if (hasImageBytes != other.hasImageBytes) return false; if (downloadUrl != other.downloadUrl) return false; @@ -554,6 +610,8 @@ class PaintModel with Equatable, SerializableMixin, FieldsHolder { blendMode, gradientTransform, gradientStops, + tileMode, + gradientExtent, imageTransform, hasImageBytes, downloadUrl, diff --git a/lib/src/api/models/paint.g.dart b/lib/src/api/models/paint.g.dart index e0ee399f..a6e8717a 100644 --- a/lib/src/api/models/paint.g.dart +++ b/lib/src/api/models/paint.g.dart @@ -14,6 +14,8 @@ Map _$PaintModelOmitDefaults( if (isJsonDefault(instance.opacity, 1)) json.remove('opacity'); if (isJsonDefault(instance.blendMode, BlendModeC.srcOver)) json.remove('blendMode'); + if (isJsonDefault(instance.tileMode, GradientTileMode.clamp)) + json.remove('tileMode'); if (isJsonDefault(instance.fit, Fit.none)) json.remove('fit'); if (isJsonDefault(instance.alignment, AlignmentModel.none)) json.remove('alignment'); @@ -54,6 +56,14 @@ PaintModel _$PaintModelFromJson(Map json) => PaintModel( gradientStops: (json['gradientStops'] as List?) ?.map((e) => ColorStop.fromJson(e as Map)) .toList(), + tileMode: + $enumDecodeNullable( + _$GradientTileModeEnumMap, + json['tileMode'], + unknownValue: GradientTileMode.clamp, + ) ?? + GradientTileMode.clamp, + gradientExtent: (json['gradientExtent'] as num?)?.toDouble(), imageTransform: (json['imageTransform'] as List?) ?.map((e) => (e as List).map((e) => e as num).toList()) .toList(), @@ -88,6 +98,8 @@ Map _$PaintModelToJson(PaintModel instance) => 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, 'gradientTransform': ?instance.gradientTransform, 'gradientStops': ?instance.gradientStops?.map((e) => e.toJson()).toList(), + 'tileMode': _$GradientTileModeEnumMap[instance.tileMode]!, + 'gradientExtent': ?instance.gradientExtent, 'imageTransform': ?instance.imageTransform, 'downloadUrl': ?instance.downloadUrl, 'imageName': ?instance.imageName, @@ -146,6 +158,12 @@ const _$BlendModeCEnumMap = { BlendModeC.luminosity: 'luminosity', }; +const _$GradientTileModeEnumMap = { + GradientTileMode.clamp: 'clamp', + GradientTileMode.repeated: 'repeated', + GradientTileMode.mirror: 'mirror', +}; + const _$FitEnumMap = { Fit.contain: 'contain', Fit.cover: 'cover', diff --git a/test/gradient_tile_mode_test.dart b/test/gradient_tile_mode_test.dart new file mode 100644 index 00000000..13e29538 --- /dev/null +++ b/test/gradient_tile_mode_test.dart @@ -0,0 +1,63 @@ +import 'dart:convert'; + +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('a repeating gradient round trips through JSON', () { + final PaintModel paint = PaintModel.linearGradient( + id: 'stripes', + gradientTransform: const [0.7071, 0.7071, 0, -0.7071, 0.7071, 0], + gradientStops: const [ + ColorStop(color: ColorRGBA(r: 1, g: 0.8, b: 0, a: 1), position: 0), + ColorStop(color: ColorRGBA(r: 1, g: 0.8, b: 0, a: 1), position: 0.5), + ColorStop(color: ColorRGBA.black, position: 0.5), + ColorStop(color: ColorRGBA.black, position: 1), + ], + tileMode: GradientTileMode.repeated, + gradientExtent: 20, + ); + + final Map json = jsonDecode(jsonEncode(paint.toJson())); + expect(json['tileMode'], 'repeated'); + expect(json['gradientExtent'], 20); + + final PaintModel decoded = PaintModel.fromJson(json); + expect(decoded, paint); + expect(decoded.sanitize(), paint); + }); + + test('a gradient without tile settings clamps and omits them', () { + final Map json = { + 'id': 'g', + 'type': 'gradientLinear', + 'gradientStops': [ + {'color': '#FF000000', 'position': 0}, + {'color': '#FFFFFFFF', 'position': 1}, + ], + }; + final PaintModel paint = PaintModel.fromJson(json); + expect(paint.tileMode, GradientTileMode.clamp); + expect(paint.gradientExtent, isNull); + expect(paint.toJson().containsKey('tileMode'), isFalse); + expect(paint.toJson().containsKey('gradientExtent'), isFalse); + }); + + test('an unknown tile mode falls back to clamp', () { + final PaintModel paint = PaintModel.fromJson({ + 'id': 'g', + 'type': 'gradientLinear', + 'tileMode': 'decal', + }); + expect(paint.tileMode, GradientTileMode.clamp); + }); + + test('copyWith can clear the gradient extent', () { + final PaintModel paint = PaintModel.linearPaint.copyWith( + gradientExtent: 12, + ); + expect(paint.gradientExtent, 12); + expect(paint.copyWith().gradientExtent, 12); + expect(paint.copyWith(clearGradientExtent: true).gradientExtent, isNull); + }); +} From a47a73a76522f080c9acaaa9561a6ddd40e2b637 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 22:27:20 +0000 Subject: [PATCH 02/13] Add SignatureNode, a draw-to-sign pad Its value is the list of strokes drawn (each a list of [x, y] points), bindable through variables.value and settable with setValue; changed fires when a stroke ends. Properties: ink color and width, background, border (and error border), corner radius, placeholder text, color and size, sign line, error and enabled (both bindable through variables). Co-Authored-By: Claude Opus 5.5 --- lib/src/api/node_json_converter.dart | 1 + lib/src/api/nodes/nodes.dart | 1 + lib/src/api/nodes/signature_node.dart | 433 ++++++++++++++++++++++ lib/src/api/nodes/signature_node.g.dart | 266 +++++++++++++ test/nodes/input/signature_node_test.dart | 91 +++++ 5 files changed, 792 insertions(+) create mode 100644 lib/src/api/nodes/signature_node.dart create mode 100644 lib/src/api/nodes/signature_node.g.dart create mode 100644 test/nodes/input/signature_node_test.dart diff --git a/lib/src/api/node_json_converter.dart b/lib/src/api/node_json_converter.dart index 311acd7c..f0c0dfca 100644 --- a/lib/src/api/node_json_converter.dart +++ b/lib/src/api/node_json_converter.dart @@ -74,6 +74,7 @@ class NodeJsonConverter implements JsonConverter { 'badge': BadgeNode.fromJson, 'chip': ChipNode.fromJson, 'segmentedButton': SegmentedButtonNode.fromJson, + 'signature': SignatureNode.fromJson, 'external': ExternalComponentNode.fromJson, }; } diff --git a/lib/src/api/nodes/nodes.dart b/lib/src/api/nodes/nodes.dart index 440e5adc..13bcab89 100644 --- a/lib/src/api/nodes/nodes.dart +++ b/lib/src/api/nodes/nodes.dart @@ -30,6 +30,7 @@ export 'root_node.dart'; export 'row_column_node.dart'; export 'scene_node.dart'; export 'segmented_button_node.dart'; +export 'signature_node.dart'; export 'single_placeholder_node.dart'; export 'slider_node.dart'; export 'spacer_node.dart'; diff --git a/lib/src/api/nodes/signature_node.dart b/lib/src/api/nodes/signature_node.dart new file mode 100644 index 00000000..1e17c3cd --- /dev/null +++ b/lib/src/api/nodes/signature_node.dart @@ -0,0 +1,433 @@ +import 'package:json_annotation/json_annotation.dart'; + +import '../field_access.dart'; +import '../mixins.dart'; +import '../models/models.dart'; +import 'nodes.dart'; + +part 'signature_node.g.dart'; + +/// Default width of a signature pad. +const double kSignatureDefaultWidth = 320; + +/// Default height of a signature pad. +const double kSignatureDefaultHeight = 140; + +/// Default color of the ink of a signature pad. +const ColorRGBA kSignatureDefaultInkColor = ColorRGBA( + r: 0x13 / 255, + g: 0x30 / 255, + b: 0x6B / 255, + a: 1, +); + +/// Default background color of a signature pad. +const ColorRGBA kSignatureDefaultBackgroundColor = ColorRGBA( + r: 0xF8 / 255, + g: 0xF8 / 255, + b: 0xF5 / 255, + a: 1, +); + +/// Default border, sign line and placeholder color of a signature pad. +const ColorRGBA kSignatureDefaultBorderColor = ColorRGBA( + r: 0xA9 / 255, + g: 0xAD / 255, + b: 0xA5 / 255, + a: 1, +); + +/// Default border color of a signature pad that is in error. +const ColorRGBA kSignatureDefaultErrorColor = ColorRGBA( + r: 0xD3 / 255, + g: 0x30 / 255, + b: 0x2F / 255, + a: 1, +); + +/// Default placeholder of a signature pad. +const String kSignatureDefaultPlaceholder = 'SIGN HERE'; + +/// A pad the user signs on by drawing with a finger, a pen or a mouse. +/// +/// Its [value] is the list of strokes drawn, each a list of `[x, y]` points +/// in the pad's coordinates. An empty list means the pad is not signed. +/// +/// Bind `variables['value']` to a list variable to read the signature +/// (`${signature.isEmpty}`, `${= length(signature) > 0}`) and to clear it +/// (set the variable to `[]`). [TriggerType.changed] fires when a stroke +/// ends, with the strokes as its value. +@JsonSerializable() +class SignatureNode extends SceneNode with CustomPropertiesMixin { + @override + final String type = 'signature'; + + /// Holds configurable properties of the signature pad. + @override + covariant SignatureProperties properties; + + /// The strokes drawn on the pad: each stroke is a list of `[x, y]` points. + /// Empty when the pad is not signed. + @JsonKey(fromJson: strokesFromJson) + List>> value; + + /// Strictly used for previews. e.g in components panel. + SignatureNode.empty() + : properties = SignatureProperties(), + value = [], + super( + id: 'signature', + name: 'Signature', + basicBoxLocal: NodeBox( + 0, + 0, + kSignatureDefaultWidth, + kSignatureDefaultHeight, + ), + ); + + /// Strictly used for previews. e.g in components panel. + SignatureNode.fromProperties(this.properties) + : value = [], + super( + id: 'signature', + name: 'Signature', + basicBoxLocal: NodeBox( + 0, + 0, + kSignatureDefaultWidth, + kSignatureDefaultHeight, + ), + ); + + /// Creates a [SignatureNode] with the given data. + SignatureNode({ + required super.id, + required super.name, + required super.basicBoxLocal, + List>>? value, + super.outerBoxLocal, + super.retainedOuterBoxLocal, + super.visible, + super.enabled, + super.alignment, + super.padding, + super.margin, + super.rotationDegrees, + super.reactions, + super.horizontalFit, + super.verticalFit, + super.flex, + super.constraints, + super.aspectRatioLock, + super.edgePins, + super.parentID, + super.positioningMode, + // Custom properties. + required this.properties, + super.variables, + super.multipleVariables, + }) : value = value ?? []; + + /// Whether the pad holds no strokes. + bool get isEmpty => value.isEmpty; + + @override + FieldsMap generateFields() => { + ...super.generateFields(), + 'properties': ObjectFieldAccess( + 'Properties', + 'Holds configurable properties of the signature pad.', + () => properties, + (value) => properties = value, + ), + }; + + /// [TriggerType.changed] fires with the strokes when a stroke ends. + @override + List get triggerTypes => [TriggerType.changed]; + + @override + late final List propertyVariables = [ + ...super.propertyVariables, + ObjectValue(name: 'value', value: value), + BoolValue(name: 'error', value: properties.error), + BoolValue(name: 'enabled', value: properties.enabled), + ]; + + @override + List get props => [ + ...super.props, + properties, + value, + variables, + multipleVariables, + ]; + + /// Creates a [SignatureNode] from a JSON data. + factory SignatureNode.fromJson(Map json) => _$SignatureNodeFromJson(json); + + @override + Map toJson() => + _$SignatureNodeOmitDefaults(this, _$SignatureNodeToJson(this)); +} + +/// Reads strokes from JSON: a list of strokes, each a list of `[x, y]` +/// points. Malformed points are skipped. +List>> strokesFromJson(Object? json) { + if (json is! List) return []; + return [ + for (final stroke in json) + if (stroke is List) + [ + for (final point in stroke) + if (point is List && + point.length >= 2 && + point[0] is num && + point[1] is num) + [(point[0] as num).toDouble(), (point[1] as num).toDouble()], + ], + ]; +} + +/// Holds configurable properties of a [SignatureNode]. +@JsonSerializable() +class SignatureProperties extends CustomProperties with FieldsHolder { + /// Color of the ink. + ColorRGBA inkColor; + + /// Width of the ink line. + double inkWidth; + + /// Background color of the pad. + ColorRGBA backgroundColor; + + /// Color of the border of the pad. + ColorRGBA borderColor; + + /// Width of the border of the pad. + double borderWidth; + + /// Color of the border when the pad is in [error]. + ColorRGBA errorBorderColor; + + /// Width of the border when the pad is in [error]. + double errorBorderWidth; + + /// Radius of the corners of the pad. + CornerRadius cornerRadius; + + /// Text shown on the pad while it is empty. Empty shows nothing. + String placeholder; + + /// Color of the [placeholder]. + ColorRGBA placeholderColor; + + /// Font size of the [placeholder]. + double placeholderFontSize; + + /// Whether to draw a sign line with an "×" near the bottom of the pad. + bool showSignLine; + + /// Color of the sign line and its "×". + ColorRGBA signLineColor; + + /// Whether the pad shows its error border. Bind `variables['error']` to + /// show it from a validation rule. + bool error; + + /// Whether the user can draw on the pad. A pad that is not enabled still + /// shows its strokes. Bind `variables['enabled']` to lock it, e.g. once + /// the form is submitted. + bool enabled; + + /// Creates a [SignatureProperties] with the given data. + SignatureProperties({ + this.inkColor = kSignatureDefaultInkColor, + this.inkWidth = 2.6, + this.backgroundColor = kSignatureDefaultBackgroundColor, + this.borderColor = kSignatureDefaultBorderColor, + this.borderWidth = 1.5, + this.errorBorderColor = kSignatureDefaultErrorColor, + this.errorBorderWidth = 2, + this.cornerRadius = const CornerRadius.all(RadiusModel.circular(4)), + this.placeholder = kSignatureDefaultPlaceholder, + this.placeholderColor = kSignatureDefaultBorderColor, + this.placeholderFontSize = 12, + this.showSignLine = true, + this.signLineColor = kSignatureDefaultBorderColor, + this.error = false, + this.enabled = true, + }); + + /// Duplicates this [SignatureProperties] with the given data overrides. + SignatureProperties copyWith({ + ColorRGBA? inkColor, + double? inkWidth, + ColorRGBA? backgroundColor, + ColorRGBA? borderColor, + double? borderWidth, + ColorRGBA? errorBorderColor, + double? errorBorderWidth, + CornerRadius? cornerRadius, + String? placeholder, + ColorRGBA? placeholderColor, + double? placeholderFontSize, + bool? showSignLine, + ColorRGBA? signLineColor, + bool? error, + bool? enabled, + }) => SignatureProperties( + inkColor: inkColor ?? this.inkColor, + inkWidth: inkWidth ?? this.inkWidth, + backgroundColor: backgroundColor ?? this.backgroundColor, + borderColor: borderColor ?? this.borderColor, + borderWidth: borderWidth ?? this.borderWidth, + errorBorderColor: errorBorderColor ?? this.errorBorderColor, + errorBorderWidth: errorBorderWidth ?? this.errorBorderWidth, + cornerRadius: cornerRadius ?? this.cornerRadius, + placeholder: placeholder ?? this.placeholder, + placeholderColor: placeholderColor ?? this.placeholderColor, + placeholderFontSize: placeholderFontSize ?? this.placeholderFontSize, + showSignLine: showSignLine ?? this.showSignLine, + signLineColor: signLineColor ?? this.signLineColor, + error: error ?? this.error, + enabled: enabled ?? this.enabled, + ); + + @override + FieldsMap generateFields() => { + ...super.generateFields(), + 'inkColor': ColorFieldAccess( + 'Ink Color', + 'Color of the ink.', + () => inkColor, + (ColorRGBA? value) { + if (value != null) inkColor = value; + }, + ), + 'inkWidth': NumFieldAccess( + 'Ink Width', + 'Width of the ink line.', + () => inkWidth, + (double value) => inkWidth = value, + ), + 'backgroundColor': ColorFieldAccess( + 'Background Color', + 'Background color of the pad.', + () => backgroundColor, + (ColorRGBA? value) { + if (value != null) backgroundColor = value; + }, + ), + 'borderColor': ColorFieldAccess( + 'Border Color', + 'Color of the border of the pad.', + () => borderColor, + (ColorRGBA? value) { + if (value != null) borderColor = value; + }, + ), + 'borderWidth': NumFieldAccess( + 'Border Width', + 'Width of the border of the pad.', + () => borderWidth, + (double value) => borderWidth = value, + ), + 'errorBorderColor': ColorFieldAccess( + 'Error Border Color', + 'Color of the border when the pad is in error.', + () => errorBorderColor, + (ColorRGBA? value) { + if (value != null) errorBorderColor = value; + }, + ), + 'errorBorderWidth': NumFieldAccess( + 'Error Border Width', + 'Width of the border when the pad is in error.', + () => errorBorderWidth, + (double value) => errorBorderWidth = value, + ), + 'cornerRadius': RadiusFieldAccess( + 'Corner Radius', + 'Radius of the corners of the pad.', + () => cornerRadius, + (CornerRadius value) => cornerRadius = value, + ), + 'placeholder': StringFieldAccess( + 'Placeholder', + 'Text shown on the pad while it is empty.', + () => placeholder, + (String value) => placeholder = value, + ), + 'placeholderColor': ColorFieldAccess( + 'Placeholder Color', + 'Color of the placeholder.', + () => placeholderColor, + (ColorRGBA? value) { + if (value != null) placeholderColor = value; + }, + ), + 'placeholderFontSize': NumFieldAccess( + 'Placeholder Font Size', + 'Font size of the placeholder.', + () => placeholderFontSize, + (double value) => placeholderFontSize = value, + ), + 'showSignLine': BoolFieldAccess( + 'Show Sign Line', + 'Whether to draw a sign line with an "×" near the bottom of the pad.', + () => showSignLine, + (bool value) => showSignLine = value, + ), + 'signLineColor': ColorFieldAccess( + 'Sign Line Color', + 'Color of the sign line and its "×".', + () => signLineColor, + (ColorRGBA? value) { + if (value != null) signLineColor = value; + }, + ), + 'error': BoolFieldAccess( + 'Error', + 'Whether the pad shows its error border.', + () => error, + (bool value) => error = value, + ), + 'enabled': BoolFieldAccess( + 'Enabled', + 'Whether the user can draw on the pad.', + () => enabled, + (bool value) => enabled = value, + ), + }; + + @override + List get props => [ + inkColor, + inkWidth, + backgroundColor, + borderColor, + borderWidth, + errorBorderColor, + errorBorderWidth, + cornerRadius, + placeholder, + placeholderColor, + placeholderFontSize, + showSignLine, + signLineColor, + error, + enabled, + ]; + + /// Creates a [SignatureProperties] from a JSON data. + factory SignatureProperties.fromJson(Map json) => + _$SignaturePropertiesFromJson(json); + + @override + Map toJson() => _$SignaturePropertiesOmitDefaults( + this, + _$SignaturePropertiesToJson(this), + ); +} diff --git a/lib/src/api/nodes/signature_node.g.dart b/lib/src/api/nodes/signature_node.g.dart new file mode 100644 index 00000000..b64511d5 --- /dev/null +++ b/lib/src/api/nodes/signature_node.g.dart @@ -0,0 +1,266 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'signature_node.dart'; + +// ************************************************************************** +// JsonDefaultsGenerator +// ************************************************************************** + +Map _$SignatureNodeOmitDefaults( + SignatureNode instance, + Map json, +) { + if (isJsonDefault(instance.value, [])) json.remove('value'); + if (isJsonDefault(instance.visible, true)) json.remove('visible'); + if (excludeConstraintsIf(instance)) json.remove('constraints'); + if (isJsonDefault(instance.constraints, const BoxConstraintsModel())) + json.remove('constraints'); + if (isJsonDefault(instance.edgePins, EdgePinsModel.standard)) + json.remove('edgePins'); + if (isJsonDefault(instance.positioningMode, PositioningMode.align)) + json.remove('positioningMode'); + if (isJsonDefault(instance.horizontalFit, SizeFit.fixed)) + json.remove('horizontalFit'); + if (isJsonDefault(instance.verticalFit, SizeFit.fixed)) + json.remove('verticalFit'); + if (isJsonDefault(instance.flex, 1)) json.remove('flex'); + if (isJsonDefault(instance.aspectRatioLock, false)) + json.remove('aspectRatioLock'); + if (isJsonDefault(instance.alignment, AlignmentModel.none)) + json.remove('alignment'); + if (isJsonDefault(instance.margin, EdgeInsetsModel.zero)) + json.remove('margin'); + if (isJsonDefault(instance.padding, EdgeInsetsModel.zero)) + json.remove('padding'); + if (isJsonDefault(instance.rotationDegrees, 0)) json.remove('rotation'); + if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); + if (isJsonDefault(instance.variables, {})) json.remove('variables'); + if (isJsonDefault(instance.multipleVariables, {})) + json.remove('multipleVariables'); + if (isJsonDefault(instance.componentVersion, 1)) + json.remove('componentVersion'); + if (isJsonDefault(instance.componentSchema, {})) + json.remove('componentSchema'); + if (isJsonDefault(instance.reactions, const [])) json.remove('reactions'); + return json; +} + +Map _$SignaturePropertiesOmitDefaults( + SignatureProperties instance, + Map json, +) { + if (isJsonDefault(instance.inkColor, kSignatureDefaultInkColor)) + json.remove('inkColor'); + if (isJsonDefault(instance.inkWidth, 2.6)) json.remove('inkWidth'); + if (isJsonDefault(instance.backgroundColor, kSignatureDefaultBackgroundColor)) + json.remove('backgroundColor'); + if (isJsonDefault(instance.borderColor, kSignatureDefaultBorderColor)) + json.remove('borderColor'); + if (isJsonDefault(instance.borderWidth, 1.5)) json.remove('borderWidth'); + if (isJsonDefault(instance.errorBorderColor, kSignatureDefaultErrorColor)) + json.remove('errorBorderColor'); + if (isJsonDefault(instance.errorBorderWidth, 2)) + json.remove('errorBorderWidth'); + if (isJsonDefault( + instance.cornerRadius, + const CornerRadius.all(RadiusModel.circular(4)), + )) + json.remove('cornerRadius'); + if (isJsonDefault(instance.placeholder, kSignatureDefaultPlaceholder)) + json.remove('placeholder'); + if (isJsonDefault(instance.placeholderColor, kSignatureDefaultBorderColor)) + json.remove('placeholderColor'); + if (isJsonDefault(instance.placeholderFontSize, 12)) + json.remove('placeholderFontSize'); + if (isJsonDefault(instance.showSignLine, true)) json.remove('showSignLine'); + if (isJsonDefault(instance.signLineColor, kSignatureDefaultBorderColor)) + json.remove('signLineColor'); + if (isJsonDefault(instance.error, false)) json.remove('error'); + if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); + return json; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SignatureNode _$SignatureNodeFromJson(Map json) => + SignatureNode( + id: json['id'] as String, + name: json['name'] as String, + basicBoxLocal: NodeBox.fromJson(json['basicBoxLocal']), + value: strokesFromJson(json['value']), + visible: json['visible'] as bool? ?? true, + enabled: json['enabled'] as bool? ?? true, + alignment: json['alignment'] == null + ? AlignmentModel.none + : AlignmentModel.fromJson(json['alignment'] as Map), + padding: json['padding'] == null + ? EdgeInsetsModel.zero + : EdgeInsetsModel.fromJson(json['padding']), + margin: json['margin'] == null + ? EdgeInsetsModel.zero + : EdgeInsetsModel.fromJson(json['margin']), + rotationDegrees: json['rotation'] == null + ? 0 + : castRotation(json['rotation']), + reactions: + (json['reactions'] as List?) + ?.map((e) => Reaction.fromJson(e as Map)) + .toList() ?? + const [], + horizontalFit: + $enumDecodeNullable(_$SizeFitEnumMap, json['horizontalFit']) ?? + SizeFit.fixed, + verticalFit: + $enumDecodeNullable(_$SizeFitEnumMap, json['verticalFit']) ?? + SizeFit.fixed, + flex: (json['flex'] as num?)?.toInt() ?? 1, + constraints: json['constraints'] == null + ? const BoxConstraintsModel() + : BoxConstraintsModel.fromJson(json['constraints'] as Map), + aspectRatioLock: json['aspectRatioLock'] as bool? ?? false, + edgePins: json['edgePins'] == null + ? EdgePinsModel.standard + : EdgePinsModel.fromJson( + Map.from(json['edgePins'] as Map), + ), + positioningMode: + $enumDecodeNullable( + _$PositioningModeEnumMap, + json['positioningMode'], + ) ?? + PositioningMode.align, + properties: SignatureProperties.fromJson(json['properties'] as Map), + variables: + (json['variables'] as Map?)?.map( + (k, e) => MapEntry(k as String, e as String), + ) ?? + {}, + multipleVariables: + (json['multipleVariables'] as Map?)?.map( + (k, e) => MapEntry( + k as String, + (e as List).map((e) => e as String).toList(), + ), + ) ?? + {}, + ) + ..componentId = json['componentId'] as String? + ..markerType = $enumDecodeNullable( + _$ComponentMarkerTypeEnumMap, + json['markerType'], + ) + ..componentVersion = (json['componentVersion'] as num?)?.toInt() ?? 1 + ..componentSchema = + (json['componentSchema'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + ) ?? + {} + ..widthFactor = (json['widthFactor'] as num?)?.toDouble() + ..heightFactor = (json['heightFactor'] as num?)?.toDouble() + ..type = json['type'] as String; + +Map _$SignatureNodeToJson(SignatureNode instance) => + { + 'variables': instance.variables, + 'multipleVariables': instance.multipleVariables, + 'componentId': ?instance.componentId, + 'markerType': ?_$ComponentMarkerTypeEnumMap[instance.markerType], + 'componentVersion': instance.componentVersion, + 'componentSchema': instance.componentSchema, + 'id': instance.id, + 'name': instance.name, + 'visible': instance.visible, + 'constraints': instance.constraints.toJson(), + 'edgePins': instance.edgePins.toJson(), + 'positioningMode': _$PositioningModeEnumMap[instance.positioningMode]!, + 'horizontalFit': _$SizeFitEnumMap[instance.horizontalFit]!, + 'verticalFit': _$SizeFitEnumMap[instance.verticalFit]!, + 'flex': instance.flex, + 'aspectRatioLock': instance.aspectRatioLock, + 'alignment': instance.alignment.toJson(), + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), + 'basicBoxLocal': instance.basicBoxLocal.toJson(), + 'margin': instance.margin.toJson(), + 'padding': instance.padding.toJson(), + 'rotation': instance.rotationDegrees, + 'widthFactor': ?instance.widthFactor, + 'heightFactor': ?instance.heightFactor, + 'enabled': instance.enabled, + 'type': instance.type, + 'properties': instance.properties.toJson(), + 'value': instance.value, + }; + +const _$SizeFitEnumMap = { + SizeFit.locked: 'locked', + SizeFit.fixed: 'fixed', + SizeFit.expanded: 'expanded', + SizeFit.flexible: 'flexible', + SizeFit.shrinkWrap: 'shrinkWrap', +}; + +const _$PositioningModeEnumMap = { + PositioningMode.align: 'align', + PositioningMode.pin: 'pin', +}; + +const _$ComponentMarkerTypeEnumMap = { + ComponentMarkerType.component: 'component', + ComponentMarkerType.instance: 'instance', +}; + +SignatureProperties _$SignaturePropertiesFromJson( + Map json, +) => SignatureProperties( + inkColor: json['inkColor'] == null + ? kSignatureDefaultInkColor + : ColorRGBA.fromJson(json['inkColor']), + inkWidth: (json['inkWidth'] as num?)?.toDouble() ?? 2.6, + backgroundColor: json['backgroundColor'] == null + ? kSignatureDefaultBackgroundColor + : ColorRGBA.fromJson(json['backgroundColor']), + borderColor: json['borderColor'] == null + ? kSignatureDefaultBorderColor + : ColorRGBA.fromJson(json['borderColor']), + borderWidth: (json['borderWidth'] as num?)?.toDouble() ?? 1.5, + errorBorderColor: json['errorBorderColor'] == null + ? kSignatureDefaultErrorColor + : ColorRGBA.fromJson(json['errorBorderColor']), + errorBorderWidth: (json['errorBorderWidth'] as num?)?.toDouble() ?? 2, + cornerRadius: json['cornerRadius'] == null + ? const CornerRadius.all(RadiusModel.circular(4)) + : CornerRadius.fromJson(json['cornerRadius']), + placeholder: json['placeholder'] as String? ?? kSignatureDefaultPlaceholder, + placeholderColor: json['placeholderColor'] == null + ? kSignatureDefaultBorderColor + : ColorRGBA.fromJson(json['placeholderColor']), + placeholderFontSize: (json['placeholderFontSize'] as num?)?.toDouble() ?? 12, + showSignLine: json['showSignLine'] as bool? ?? true, + signLineColor: json['signLineColor'] == null + ? kSignatureDefaultBorderColor + : ColorRGBA.fromJson(json['signLineColor']), + error: json['error'] as bool? ?? false, + enabled: json['enabled'] as bool? ?? true, +); + +Map _$SignaturePropertiesToJson( + SignatureProperties instance, +) => { + 'inkColor': instance.inkColor.toJson(), + 'inkWidth': instance.inkWidth, + 'backgroundColor': instance.backgroundColor.toJson(), + 'borderColor': instance.borderColor.toJson(), + 'borderWidth': instance.borderWidth, + 'errorBorderColor': instance.errorBorderColor.toJson(), + 'errorBorderWidth': instance.errorBorderWidth, + 'cornerRadius': instance.cornerRadius.toJson(), + 'placeholder': instance.placeholder, + 'placeholderColor': instance.placeholderColor.toJson(), + 'placeholderFontSize': instance.placeholderFontSize, + 'showSignLine': instance.showSignLine, + 'signLineColor': instance.signLineColor.toJson(), + 'error': instance.error, + 'enabled': instance.enabled, +}; diff --git a/test/nodes/input/signature_node_test.dart b/test/nodes/input/signature_node_test.dart new file mode 100644 index 00000000..787b09e4 --- /dev/null +++ b/test/nodes/input/signature_node_test.dart @@ -0,0 +1,91 @@ +import 'dart:convert'; + +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +/// Encodes [node] to JSON text and reads it back through the registry, the +/// way a stored layout is loaded. +BaseNode roundTrip(BaseNode node) => NodeJsonConverter.instance.fromJson( + jsonDecode(jsonEncode(node.toJson())) as Map, +)!; + +void main() { + test('a default signature pad writes only what it needs', () { + final json = SignatureNode.empty().toJson(); + expect(json['type'], 'signature'); + expect(json.containsKey('value'), isFalse); + expect(json['properties'], isEmpty); + + final read = roundTrip(SignatureNode.empty()) as SignatureNode; + expect(read.value, isEmpty); + expect(read.properties, SignatureProperties()); + expect(read.basicBoxLocal.width, kSignatureDefaultWidth); + expect(read.basicBoxLocal.height, kSignatureDefaultHeight); + }); + + test('a signature pad keeps its strokes, properties and bindings', () { + final node = SignatureNode( + id: 'sig', + name: 'Signature', + basicBoxLocal: NodeBox(0, 0, 320, 140), + value: [ + [ + [10, 20], + [30.5, 40], + ], + [ + [50, 60], + ], + ], + variables: {'value': r'${signature}', 'error': r'${signatureError}'}, + reactions: [ + Reaction( + trigger: TriggerModel(type: TriggerType.changed), + action: SetVariableAction( + variable: VariableData(id: 'signed', name: 'signed'), + newValue: 'true', + ), + ), + ], + properties: SignatureProperties( + inkColor: ColorRGBA.black, + inkWidth: 3, + placeholder: 'Sign', + showSignLine: false, + error: true, + enabled: false, + ), + ); + + final read = roundTrip(node) as SignatureNode; + expect(read.value, node.value); + expect(read.properties, node.properties); + expect(read.variables, node.variables); + expect(read.reactions.single.trigger.type, TriggerType.changed); + expect(read.triggerTypes, [TriggerType.changed]); + expect(read.toJson(), node.toJson()); + expect( + read.propertyVariables.map((value) => value.name), + containsAll(['value', 'error', 'enabled']), + ); + }); + + test('strokes skip malformed points', () { + expect( + strokesFromJson([ + [ + [1, 2], + ['x', 3], + [4], + ], + 'nope', + ]), + [ + [ + [1.0, 2.0], + ], + ], + ); + expect(strokesFromJson(null), isEmpty); + }); +} From a4947f8096a4eb92771e6d1a83b60b03133c26a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 22:28:11 +0000 Subject: [PATCH 03/13] Loop animations on any node; cursor triggers on clickable nodes BaseNode gains an optional loopAnimation (LoopAnimation: pulse, fade, scale or spin, with duration, delay, reverse, curve and from/to scale, opacity or turns), serialized on every node type and left out when null. Its `loopAnimationEnabled` field can be bound to a variable to turn it on and off at runtime. Nodes with a click trigger now offer cursorEnter and cursorExit, and the trigger docs say what hover, cursorEnter and cursorExit mean. Co-Authored-By: Claude Opus 5.5 --- lib/src/api/models/loop_animation.dart | 225 ++++++++++++++++++ lib/src/api/models/loop_animation.g.dart | 84 +++++++ lib/src/api/models/models.dart | 1 + lib/src/api/models/trigger.dart | 10 +- lib/src/api/nodes/accordion_node.g.dart | 4 + lib/src/api/nodes/app_bar_node.g.dart | 4 + .../api/nodes/auto_placeholder_node.g.dart | 4 + lib/src/api/nodes/badge_node.dart | 6 +- lib/src/api/nodes/badge_node.g.dart | 4 + lib/src/api/nodes/base_node.dart | 12 + lib/src/api/nodes/button_node.dart | 2 + lib/src/api/nodes/button_node.g.dart | 4 + lib/src/api/nodes/canvas_node.g.dart | 4 + lib/src/api/nodes/checkbox_node.g.dart | 4 + lib/src/api/nodes/chip_node.dart | 2 + lib/src/api/nodes/chip_node.g.dart | 4 + lib/src/api/nodes/divider_node.g.dart | 4 + lib/src/api/nodes/dropdown_node.dart | 2 + lib/src/api/nodes/dropdown_node.g.dart | 4 + lib/src/api/nodes/embedded_video_node.g.dart | 4 + lib/src/api/nodes/expansion_tile_node.g.dart | 6 +- .../api/nodes/external_component_node.dart | 2 + .../api/nodes/external_component_node.g.dart | 4 + .../nodes/floating_action_button_node.dart | 6 +- .../nodes/floating_action_button_node.g.dart | 4 + lib/src/api/nodes/frame_node.dart | 2 + lib/src/api/nodes/frame_node.g.dart | 4 + .../nodes/freeform_placeholder_node.g.dart | 4 + lib/src/api/nodes/grid_view_node.g.dart | 6 +- lib/src/api/nodes/icon_node.dart | 2 + lib/src/api/nodes/icon_node.g.dart | 4 + lib/src/api/nodes/list_tile_node.dart | 2 + lib/src/api/nodes/list_tile_node.g.dart | 4 + lib/src/api/nodes/list_view_node.g.dart | 6 +- .../api/nodes/loading_indicator_node.g.dart | 4 + lib/src/api/nodes/navigation_bar_node.g.dart | 4 + lib/src/api/nodes/page_view_node.g.dart | 4 + lib/src/api/nodes/placeholder_node.g.dart | 4 + lib/src/api/nodes/progressbar_node.g.dart | 4 + lib/src/api/nodes/radio_node.dart | 6 +- lib/src/api/nodes/radio_node.g.dart | 6 +- lib/src/api/nodes/rectangle_node.dart | 2 + lib/src/api/nodes/rectangle_node.g.dart | 18 +- lib/src/api/nodes/row_column_node.g.dart | 4 + .../api/nodes/segmented_button_node.g.dart | 4 + lib/src/api/nodes/signature_node.g.dart | 34 +++ .../api/nodes/single_placeholder_node.g.dart | 4 + lib/src/api/nodes/slider_node.g.dart | 4 + lib/src/api/nodes/spacer_node.g.dart | 4 + lib/src/api/nodes/switch_node.g.dart | 4 + lib/src/api/nodes/tab_bar_node.g.dart | 4 + lib/src/api/nodes/text_field_node.g.dart | 4 + lib/src/api/nodes/text_node.dart | 2 + lib/src/api/nodes/text_node.g.dart | 4 + lib/src/api/nodes/variance_node.g.dart | 4 + lib/src/api/nodes/web_view_node.g.dart | 4 + test/loop_animation_test.dart | 157 ++++++++++++ 57 files changed, 716 insertions(+), 17 deletions(-) create mode 100644 lib/src/api/models/loop_animation.dart create mode 100644 lib/src/api/models/loop_animation.g.dart create mode 100644 test/loop_animation_test.dart diff --git a/lib/src/api/models/loop_animation.dart b/lib/src/api/models/loop_animation.dart new file mode 100644 index 00000000..776d7b2f --- /dev/null +++ b/lib/src/api/models/loop_animation.dart @@ -0,0 +1,225 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../mixins.dart'; +import 'models.dart'; + +part 'loop_animation.g.dart'; + +/// What a [LoopAnimation] animates. +enum LoopAnimationType { + /// Scales the node from [LoopAnimation.scaleFrom] to + /// [LoopAnimation.scaleTo] while fading it from [LoopAnimation.opacityFrom] + /// to [LoopAnimation.opacityTo]: a halo that grows and fades out. + pulse, + + /// Fades the node from [LoopAnimation.opacityFrom] to + /// [LoopAnimation.opacityTo]: a skeleton bar that breathes. + fade, + + /// Scales the node from [LoopAnimation.scaleFrom] to + /// [LoopAnimation.scaleTo]. + scale, + + /// Rotates the node by [LoopAnimation.turns] full turns each cycle: a + /// spinner. + spin; + + /// Displayable string representation of the type. + String get prettify => switch (this) { + LoopAnimationType.pulse => 'Pulse', + LoopAnimationType.fade => 'Fade', + LoopAnimationType.scale => 'Scale', + LoopAnimationType.spin => 'Spin', + }; + + /// Whether this type animates the node's scale. + bool get scales => this == pulse || this == scale; + + /// Whether this type animates the node's opacity. + bool get fades => this == pulse || this == fade; +} + +/// The easing curve of a [LoopAnimation]. Mirrors Flutter's `Curves`. +enum LoopAnimationCurve { + /// `Curves.linear`. + linear, + + /// `Curves.easeIn`. + easeIn, + + /// `Curves.easeOut`. + easeOut, + + /// `Curves.easeInOut`. + easeInOut, + + /// `Curves.fastOutSlowIn`. + fastOutSlowIn, + + /// `Curves.decelerate`. + decelerate; + + /// Displayable string representation of the curve. + String get prettify => switch (this) { + LoopAnimationCurve.linear => 'Linear', + LoopAnimationCurve.easeIn => 'Ease In', + LoopAnimationCurve.easeOut => 'Ease Out', + LoopAnimationCurve.easeInOut => 'Ease In Out', + LoopAnimationCurve.fastOutSlowIn => 'Fast Out Slow In', + LoopAnimationCurve.decelerate => 'Decelerate', + }; +} + +/// An animation that repeats for as long as its node is shown: a pulsing +/// halo, a breathing skeleton bar, a spinning icon. +/// +/// Each cycle runs from the `from` values to the `to` values in +/// [durationMs]. With [reverse], it then runs back to the `from` values; +/// otherwise it jumps back and starts over. +/// +/// The node's own opacity, rotation and size stay as designed; the animated +/// values multiply them. The design canvas holds the animation still at its +/// `from` values; previews and published apps run it. +/// +/// Bind the node's `loopAnimationEnabled` variable to turn it on and off at +/// runtime. +@JsonSerializable() +class LoopAnimation with Equatable, SerializableMixin { + /// What this animation animates. + @JsonKey(unknownEnumValue: LoopAnimationType.pulse) + final LoopAnimationType type; + + /// Whether the animation runs. A disabled animation leaves the node as + /// designed. + final bool enabled; + + /// How long one cycle takes, in milliseconds. With [reverse], running back + /// takes as long again. + final int durationMs; + + /// How long to wait before the first cycle, in milliseconds. + final int delayMs; + + /// Whether each cycle runs back to the `from` values instead of jumping + /// back to them. + final bool reverse; + + /// The easing curve of each cycle. + @JsonKey(unknownEnumValue: LoopAnimationCurve.linear) + final LoopAnimationCurve curve; + + /// The scale at the start of a cycle, for [LoopAnimationType.pulse] and + /// [LoopAnimationType.scale]. The node scales about its center. + final double scaleFrom; + + /// The scale at the end of a cycle. + final double scaleTo; + + /// The opacity at the start of a cycle, for [LoopAnimationType.pulse] and + /// [LoopAnimationType.fade]. + final double opacityFrom; + + /// The opacity at the end of a cycle. + final double opacityTo; + + /// How many full turns the node rotates each cycle, for + /// [LoopAnimationType.spin]. Negative turns rotate counterclockwise. + final double turns; + + /// Creates a [LoopAnimation]. + const LoopAnimation({ + required this.type, + this.enabled = true, + this.durationMs = 1000, + this.delayMs = 0, + this.reverse = false, + this.curve = LoopAnimationCurve.linear, + this.scaleFrom = 1, + this.scaleTo = 1, + this.opacityFrom = 1, + this.opacityTo = 1, + this.turns = 1, + }); + + /// A [LoopAnimation] of [type] with values that show it off: a halo that + /// grows and fades out, a bar that breathes, a node that spins once a + /// second. + factory LoopAnimation.preset(LoopAnimationType type) => switch (type) { + LoopAnimationType.pulse => const LoopAnimation( + type: LoopAnimationType.pulse, + durationMs: 1400, + curve: LoopAnimationCurve.easeOut, + scaleFrom: 0.65, + scaleTo: 1, + opacityFrom: 0.35, + opacityTo: 0, + ), + LoopAnimationType.fade => const LoopAnimation( + type: LoopAnimationType.fade, + durationMs: 900, + reverse: true, + curve: LoopAnimationCurve.easeInOut, + opacityFrom: 0.45, + opacityTo: 1, + ), + LoopAnimationType.scale => const LoopAnimation( + type: LoopAnimationType.scale, + durationMs: 900, + reverse: true, + curve: LoopAnimationCurve.easeInOut, + scaleFrom: 1, + scaleTo: 1.1, + ), + LoopAnimationType.spin => const LoopAnimation(type: LoopAnimationType.spin), + }; + + /// Duplicates this instance with the given data overrides. + LoopAnimation copyWith({ + LoopAnimationType? type, + bool? enabled, + int? durationMs, + int? delayMs, + bool? reverse, + LoopAnimationCurve? curve, + double? scaleFrom, + double? scaleTo, + double? opacityFrom, + double? opacityTo, + double? turns, + }) => LoopAnimation( + type: type ?? this.type, + enabled: enabled ?? this.enabled, + durationMs: durationMs ?? this.durationMs, + delayMs: delayMs ?? this.delayMs, + reverse: reverse ?? this.reverse, + curve: curve ?? this.curve, + scaleFrom: scaleFrom ?? this.scaleFrom, + scaleTo: scaleTo ?? this.scaleTo, + opacityFrom: opacityFrom ?? this.opacityFrom, + opacityTo: opacityTo ?? this.opacityTo, + turns: turns ?? this.turns, + ); + + @override + List get props => [ + type, + enabled, + durationMs, + delayMs, + reverse, + curve, + scaleFrom, + scaleTo, + opacityFrom, + opacityTo, + turns, + ]; + + /// Creates a [LoopAnimation] from JSON data. + factory LoopAnimation.fromJson(Map json) => _$LoopAnimationFromJson(json); + + @override + Map toJson() => + _$LoopAnimationOmitDefaults(this, _$LoopAnimationToJson(this)); +} diff --git a/lib/src/api/models/loop_animation.g.dart b/lib/src/api/models/loop_animation.g.dart new file mode 100644 index 00000000..8d3fc403 --- /dev/null +++ b/lib/src/api/models/loop_animation.g.dart @@ -0,0 +1,84 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'loop_animation.dart'; + +// ************************************************************************** +// JsonDefaultsGenerator +// ************************************************************************** + +Map _$LoopAnimationOmitDefaults( + LoopAnimation instance, + Map json, +) { + if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); + if (isJsonDefault(instance.durationMs, 1000)) json.remove('durationMs'); + if (isJsonDefault(instance.delayMs, 0)) json.remove('delayMs'); + if (isJsonDefault(instance.reverse, false)) json.remove('reverse'); + if (isJsonDefault(instance.curve, LoopAnimationCurve.linear)) + json.remove('curve'); + if (isJsonDefault(instance.scaleFrom, 1)) json.remove('scaleFrom'); + if (isJsonDefault(instance.scaleTo, 1)) json.remove('scaleTo'); + if (isJsonDefault(instance.opacityFrom, 1)) json.remove('opacityFrom'); + if (isJsonDefault(instance.opacityTo, 1)) json.remove('opacityTo'); + if (isJsonDefault(instance.turns, 1)) json.remove('turns'); + return json; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LoopAnimation _$LoopAnimationFromJson(Map json) => LoopAnimation( + type: $enumDecode( + _$LoopAnimationTypeEnumMap, + json['type'], + unknownValue: LoopAnimationType.pulse, + ), + enabled: json['enabled'] as bool? ?? true, + durationMs: (json['durationMs'] as num?)?.toInt() ?? 1000, + delayMs: (json['delayMs'] as num?)?.toInt() ?? 0, + reverse: json['reverse'] as bool? ?? false, + curve: + $enumDecodeNullable( + _$LoopAnimationCurveEnumMap, + json['curve'], + unknownValue: LoopAnimationCurve.linear, + ) ?? + LoopAnimationCurve.linear, + scaleFrom: (json['scaleFrom'] as num?)?.toDouble() ?? 1, + scaleTo: (json['scaleTo'] as num?)?.toDouble() ?? 1, + opacityFrom: (json['opacityFrom'] as num?)?.toDouble() ?? 1, + opacityTo: (json['opacityTo'] as num?)?.toDouble() ?? 1, + turns: (json['turns'] as num?)?.toDouble() ?? 1, +); + +Map _$LoopAnimationToJson(LoopAnimation instance) => + { + 'type': _$LoopAnimationTypeEnumMap[instance.type]!, + 'enabled': instance.enabled, + 'durationMs': instance.durationMs, + 'delayMs': instance.delayMs, + 'reverse': instance.reverse, + 'curve': _$LoopAnimationCurveEnumMap[instance.curve]!, + 'scaleFrom': instance.scaleFrom, + 'scaleTo': instance.scaleTo, + 'opacityFrom': instance.opacityFrom, + 'opacityTo': instance.opacityTo, + 'turns': instance.turns, + }; + +const _$LoopAnimationTypeEnumMap = { + LoopAnimationType.pulse: 'pulse', + LoopAnimationType.fade: 'fade', + LoopAnimationType.scale: 'scale', + LoopAnimationType.spin: 'spin', +}; + +const _$LoopAnimationCurveEnumMap = { + LoopAnimationCurve.linear: 'linear', + LoopAnimationCurve.easeIn: 'easeIn', + LoopAnimationCurve.easeOut: 'easeOut', + LoopAnimationCurve.easeInOut: 'easeInOut', + LoopAnimationCurve.fastOutSlowIn: 'fastOutSlowIn', + LoopAnimationCurve.decelerate: 'decelerate', +}; diff --git a/lib/src/api/models/models.dart b/lib/src/api/models/models.dart index 3b86dfac..7bbdbfd0 100644 --- a/lib/src/api/models/models.dart +++ b/lib/src/api/models/models.dart @@ -52,6 +52,7 @@ export 'input_decoration.dart'; export 'letter_spacing.dart'; export 'line_height.dart'; export 'list_tile_control_affinity.dart'; +export 'loop_animation.dart'; export 'navigation_bar_item.dart'; export 'node_box.dart'; export 'paint.dart'; diff --git a/lib/src/api/models/trigger.dart b/lib/src/api/models/trigger.dart index 8b75dbdd..ad248214 100644 --- a/lib/src/api/models/trigger.dart +++ b/lib/src/api/models/trigger.dart @@ -24,16 +24,20 @@ enum TriggerType { /// Triggered when the user long presses on a target. longPress, - /// Triggered when the user hovers over a target with mouse cursor. + /// Triggered each time the mouse cursor moves over the target, after it + /// entered. Use [cursorEnter] and [cursorExit] to track whether the cursor + /// is over the target: they fire once each. hover, /// Triggered when the user drags the target. drag, - /// Triggered when the cursor enters the target region. + /// Triggered once when the mouse cursor enters the target region. Touch + /// input doesn't fire it. cursorEnter, - /// Triggered when the cursor exits the target region. + /// Triggered once when the mouse cursor exits the target region. Touch + /// input doesn't fire it. cursorExit, /// Triggered when a pointer down event is received on the target. diff --git a/lib/src/api/nodes/accordion_node.g.dart b/lib/src/api/nodes/accordion_node.g.dart index 77f4f620..8ea864ce 100644 --- a/lib/src/api/nodes/accordion_node.g.dart +++ b/lib/src/api/nodes/accordion_node.g.dart @@ -179,6 +179,9 @@ AccordionNode _$AccordionNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$AccordionNodeToJson( @@ -213,6 +216,7 @@ Map _$AccordionNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'rowColumnType': _$RowColumnTypeEnumMap[instance.rowColumnType]!, diff --git a/lib/src/api/nodes/app_bar_node.g.dart b/lib/src/api/nodes/app_bar_node.g.dart index de0fa8ba..ef128423 100644 --- a/lib/src/api/nodes/app_bar_node.g.dart +++ b/lib/src/api/nodes/app_bar_node.g.dart @@ -162,6 +162,9 @@ AppBarNode _$AppBarNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$AppBarNodeToJson(AppBarNode instance) => @@ -195,6 +198,7 @@ Map _$AppBarNodeToJson(AppBarNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/auto_placeholder_node.g.dart b/lib/src/api/nodes/auto_placeholder_node.g.dart index 29e36b8f..ff82849d 100644 --- a/lib/src/api/nodes/auto_placeholder_node.g.dart +++ b/lib/src/api/nodes/auto_placeholder_node.g.dart @@ -231,6 +231,9 @@ AutoPlaceholderNode _$AutoPlaceholderNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -270,6 +273,7 @@ Map _$AutoPlaceholderNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'opacity': instance.opacity, diff --git a/lib/src/api/nodes/badge_node.dart b/lib/src/api/nodes/badge_node.dart index a72c9e2f..3c053fec 100644 --- a/lib/src/api/nodes/badge_node.dart +++ b/lib/src/api/nodes/badge_node.dart @@ -95,7 +95,11 @@ class BadgeNode extends SceneNode with CustomPropertiesMixin { }; @override - List get triggerTypes => [TriggerType.click]; + List get triggerTypes => [ + TriggerType.click, + TriggerType.cursorEnter, + TriggerType.cursorExit, + ]; @override late final List propertyVariables = [ diff --git a/lib/src/api/nodes/badge_node.g.dart b/lib/src/api/nodes/badge_node.g.dart index 675e8d90..fecc3737 100644 --- a/lib/src/api/nodes/badge_node.g.dart +++ b/lib/src/api/nodes/badge_node.g.dart @@ -164,6 +164,9 @@ BadgeNode _$BadgeNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$BadgeNodeToJson(BadgeNode instance) => { @@ -196,6 +199,7 @@ Map _$BadgeNodeToJson(BadgeNode instance) => { ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/base_node.dart b/lib/src/api/nodes/base_node.dart index f37c42ec..eb4f3b8b 100644 --- a/lib/src/api/nodes/base_node.dart +++ b/lib/src/api/nodes/base_node.dart @@ -388,6 +388,12 @@ abstract class BaseNode @JsonKey(defaultValue: false) bool zoomable = false; + /// An animation that repeats while this node is shown, e.g. a pulsing + /// halo or a spinning icon. Null when the node doesn't animate. + /// + /// See [LoopAnimation]. + LoopAnimation? loopAnimation; + /// Determines whether the node is enabled or not. When translating the node /// into a widget or code, this value is used to determine whether this node /// should be included and accounted for or not. @@ -780,6 +786,12 @@ abstract class BaseNode () => !enabled, (value) => enabled = !value, ), + 'loopAnimationEnabled': BoolFieldAccess( + 'Animate', + 'Whether the loop animation of this widget runs.', + () => loopAnimation?.enabled ?? false, + (value) => loopAnimation = loopAnimation?.copyWith(enabled: value), + ), }; @override diff --git a/lib/src/api/nodes/button_node.dart b/lib/src/api/nodes/button_node.dart index 76da81c8..14967e1f 100644 --- a/lib/src/api/nodes/button_node.dart +++ b/lib/src/api/nodes/button_node.dart @@ -163,6 +163,8 @@ class ButtonNode extends SceneNode with CustomPropertiesMixin { List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.cursorEnter, + TriggerType.cursorExit, ]; @override diff --git a/lib/src/api/nodes/button_node.g.dart b/lib/src/api/nodes/button_node.g.dart index 443ae349..bdc859bd 100644 --- a/lib/src/api/nodes/button_node.g.dart +++ b/lib/src/api/nodes/button_node.g.dart @@ -173,6 +173,9 @@ ButtonNode _$ButtonNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$ButtonNodeToJson(ButtonNode instance) => @@ -206,6 +209,7 @@ Map _$ButtonNodeToJson(ButtonNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/canvas_node.g.dart b/lib/src/api/nodes/canvas_node.g.dart index 70aeb784..787c24a2 100644 --- a/lib/src/api/nodes/canvas_node.g.dart +++ b/lib/src/api/nodes/canvas_node.g.dart @@ -285,6 +285,9 @@ CanvasNode _$CanvasNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -324,6 +327,7 @@ Map _$CanvasNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'opacity': instance.opacity, diff --git a/lib/src/api/nodes/checkbox_node.g.dart b/lib/src/api/nodes/checkbox_node.g.dart index 1ba0fa1d..b3cecb9e 100644 --- a/lib/src/api/nodes/checkbox_node.g.dart +++ b/lib/src/api/nodes/checkbox_node.g.dart @@ -168,6 +168,9 @@ CheckboxNode _$CheckboxNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$CheckboxNodeToJson(CheckboxNode instance) => @@ -201,6 +204,7 @@ Map _$CheckboxNodeToJson(CheckboxNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/chip_node.dart b/lib/src/api/nodes/chip_node.dart index e3570bd3..a73d43f3 100644 --- a/lib/src/api/nodes/chip_node.dart +++ b/lib/src/api/nodes/chip_node.dart @@ -97,6 +97,8 @@ class ChipNode extends SceneNode with CustomPropertiesMixin { List get triggerTypes => [ TriggerType.click, TriggerType.changed, + TriggerType.cursorEnter, + TriggerType.cursorExit, ]; @override diff --git a/lib/src/api/nodes/chip_node.g.dart b/lib/src/api/nodes/chip_node.g.dart index 4b930f59..75498f3c 100644 --- a/lib/src/api/nodes/chip_node.g.dart +++ b/lib/src/api/nodes/chip_node.g.dart @@ -162,6 +162,9 @@ ChipNode _$ChipNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$ChipNodeToJson(ChipNode instance) => { @@ -194,6 +197,7 @@ Map _$ChipNodeToJson(ChipNode instance) => { ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/divider_node.g.dart b/lib/src/api/nodes/divider_node.g.dart index aa478db5..c7ca4ddd 100644 --- a/lib/src/api/nodes/divider_node.g.dart +++ b/lib/src/api/nodes/divider_node.g.dart @@ -151,6 +151,9 @@ DividerNode _$DividerNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$DividerNodeToJson(DividerNode instance) => @@ -184,6 +187,7 @@ Map _$DividerNodeToJson(DividerNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/dropdown_node.dart b/lib/src/api/nodes/dropdown_node.dart index 2caef62b..370d1e5e 100644 --- a/lib/src/api/nodes/dropdown_node.dart +++ b/lib/src/api/nodes/dropdown_node.dart @@ -56,6 +56,8 @@ class DropdownNode extends SceneNode with CustomPropertiesMixin, FieldsHolder { List get triggerTypes => [ TriggerType.click, TriggerType.changed, + TriggerType.cursorEnter, + TriggerType.cursorExit, ]; @override diff --git a/lib/src/api/nodes/dropdown_node.g.dart b/lib/src/api/nodes/dropdown_node.g.dart index 8739ea00..48dfbc6f 100644 --- a/lib/src/api/nodes/dropdown_node.g.dart +++ b/lib/src/api/nodes/dropdown_node.g.dart @@ -179,6 +179,9 @@ DropdownNode _$DropdownNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$DropdownNodeToJson(DropdownNode instance) => @@ -212,6 +215,7 @@ Map _$DropdownNodeToJson(DropdownNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/embedded_video_node.g.dart b/lib/src/api/nodes/embedded_video_node.g.dart index 4b4ef740..2dd9770b 100644 --- a/lib/src/api/nodes/embedded_video_node.g.dart +++ b/lib/src/api/nodes/embedded_video_node.g.dart @@ -188,6 +188,9 @@ EmbeddedVideoNode _$EmbeddedVideoNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$EmbeddedVideoNodeToJson(EmbeddedVideoNode instance) => @@ -221,6 +224,7 @@ Map _$EmbeddedVideoNodeToJson(EmbeddedVideoNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/expansion_tile_node.g.dart b/lib/src/api/nodes/expansion_tile_node.g.dart index 50dde0fe..dc4125d2 100644 --- a/lib/src/api/nodes/expansion_tile_node.g.dart +++ b/lib/src/api/nodes/expansion_tile_node.g.dart @@ -193,6 +193,9 @@ ExpansionTileNode _$ExpansionTileNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..rowColumnType = $enumDecode( _$RowColumnTypeEnumMap, json['rowColumnType'], @@ -210,6 +213,7 @@ ExpansionTileNode _$ExpansionTileNodeFromJson(Map json) => Map _$ExpansionTileNodeToJson( ExpansionTileNode instance, ) => { + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -227,7 +231,6 @@ Map _$ExpansionTileNodeToJson( 'flex': instance.flex, 'aspectRatioLock': instance.aspectRatioLock, 'alignment': instance.alignment.toJson(), - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), 'padding': instance.padding.toJson(), @@ -239,6 +242,7 @@ Map _$ExpansionTileNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'rowColumnType': _$RowColumnTypeEnumMap[instance.rowColumnType]!, diff --git a/lib/src/api/nodes/external_component_node.dart b/lib/src/api/nodes/external_component_node.dart index 072951a0..20ded28c 100644 --- a/lib/src/api/nodes/external_component_node.dart +++ b/lib/src/api/nodes/external_component_node.dart @@ -56,6 +56,8 @@ class ExternalComponentNode extends SceneNode with CustomPropertiesMixin { List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.cursorEnter, + TriggerType.cursorExit, ]; @override diff --git a/lib/src/api/nodes/external_component_node.g.dart b/lib/src/api/nodes/external_component_node.g.dart index 7eeceb4a..cb608cbf 100644 --- a/lib/src/api/nodes/external_component_node.g.dart +++ b/lib/src/api/nodes/external_component_node.g.dart @@ -150,6 +150,9 @@ ExternalComponentNode _$ExternalComponentNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$ExternalComponentNodeToJson( @@ -184,6 +187,7 @@ Map _$ExternalComponentNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/floating_action_button_node.dart b/lib/src/api/nodes/floating_action_button_node.dart index 577f22cb..ddaec9a3 100644 --- a/lib/src/api/nodes/floating_action_button_node.dart +++ b/lib/src/api/nodes/floating_action_button_node.dart @@ -84,7 +84,11 @@ class FloatingActionButtonNode extends SceneNode }); @override - List get triggerTypes => [TriggerType.click]; + List get triggerTypes => [ + TriggerType.click, + TriggerType.cursorEnter, + TriggerType.cursorExit, + ]; @override FieldsMap generateFields() => { diff --git a/lib/src/api/nodes/floating_action_button_node.g.dart b/lib/src/api/nodes/floating_action_button_node.g.dart index d969096e..9155e4d9 100644 --- a/lib/src/api/nodes/floating_action_button_node.g.dart +++ b/lib/src/api/nodes/floating_action_button_node.g.dart @@ -171,6 +171,9 @@ FloatingActionButtonNode _$FloatingActionButtonNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$FloatingActionButtonNodeToJson( @@ -205,6 +208,7 @@ Map _$FloatingActionButtonNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/frame_node.dart b/lib/src/api/nodes/frame_node.dart index 6bf1a6f9..aa6419bf 100644 --- a/lib/src/api/nodes/frame_node.dart +++ b/lib/src/api/nodes/frame_node.dart @@ -107,6 +107,8 @@ class FrameNode extends DefaultShapeNode List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.cursorEnter, + TriggerType.cursorExit, ]; /// Creates a [FrameNode] from a JSON data. diff --git a/lib/src/api/nodes/frame_node.g.dart b/lib/src/api/nodes/frame_node.g.dart index 48c894de..deec42ca 100644 --- a/lib/src/api/nodes/frame_node.g.dart +++ b/lib/src/api/nodes/frame_node.g.dart @@ -257,6 +257,9 @@ FrameNode _$FrameNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -294,6 +297,7 @@ Map _$FrameNodeToJson(FrameNode instance) => { ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'opacity': instance.opacity, diff --git a/lib/src/api/nodes/freeform_placeholder_node.g.dart b/lib/src/api/nodes/freeform_placeholder_node.g.dart index 1f9d5453..78207e7e 100644 --- a/lib/src/api/nodes/freeform_placeholder_node.g.dart +++ b/lib/src/api/nodes/freeform_placeholder_node.g.dart @@ -205,6 +205,9 @@ FreeformPlaceholderNode _$FreeformPlaceholderNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -244,6 +247,7 @@ Map _$FreeformPlaceholderNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'opacity': instance.opacity, diff --git a/lib/src/api/nodes/grid_view_node.g.dart b/lib/src/api/nodes/grid_view_node.g.dart index eb9c6485..669a815f 100644 --- a/lib/src/api/nodes/grid_view_node.g.dart +++ b/lib/src/api/nodes/grid_view_node.g.dart @@ -242,6 +242,9 @@ GridViewNode _$GridViewNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..deniedTypes = (json['deniedTypes'] as List) .map((e) => e as String) .toList() @@ -266,7 +269,6 @@ GridViewNode _$GridViewNodeFromJson(Map json) => Map _$GridViewNodeToJson(GridViewNode instance) => { - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -284,6 +286,7 @@ Map _$GridViewNodeToJson(GridViewNode instance) => 'flex': instance.flex, 'aspectRatioLock': instance.aspectRatioLock, 'alignment': instance.alignment.toJson(), + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), 'padding': instance.padding.toJson(), @@ -295,6 +298,7 @@ Map _$GridViewNodeToJson(GridViewNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'deniedTypes': instance.deniedTypes, diff --git a/lib/src/api/nodes/icon_node.dart b/lib/src/api/nodes/icon_node.dart index c217b72d..da380092 100644 --- a/lib/src/api/nodes/icon_node.dart +++ b/lib/src/api/nodes/icon_node.dart @@ -104,6 +104,8 @@ class IconNode extends SceneNode List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.cursorEnter, + TriggerType.cursorExit, ]; /// Creates an [IconNode] from a JSON data. diff --git a/lib/src/api/nodes/icon_node.g.dart b/lib/src/api/nodes/icon_node.g.dart index 59174871..98f4b871 100644 --- a/lib/src/api/nodes/icon_node.g.dart +++ b/lib/src/api/nodes/icon_node.g.dart @@ -172,6 +172,9 @@ IconNode _$IconNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$IconNodeToJson(IconNode instance) => { @@ -204,6 +207,7 @@ Map _$IconNodeToJson(IconNode instance) => { ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, diff --git a/lib/src/api/nodes/list_tile_node.dart b/lib/src/api/nodes/list_tile_node.dart index b4b07378..00ddf94e 100644 --- a/lib/src/api/nodes/list_tile_node.dart +++ b/lib/src/api/nodes/list_tile_node.dart @@ -134,6 +134,8 @@ class ListTileNode extends SceneNode List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.cursorEnter, + TriggerType.cursorExit, ]; @override diff --git a/lib/src/api/nodes/list_tile_node.g.dart b/lib/src/api/nodes/list_tile_node.g.dart index 326836c6..ee5974bf 100644 --- a/lib/src/api/nodes/list_tile_node.g.dart +++ b/lib/src/api/nodes/list_tile_node.g.dart @@ -178,6 +178,9 @@ ListTileNode _$ListTileNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$ListTileNodeToJson(ListTileNode instance) => @@ -211,6 +214,7 @@ Map _$ListTileNodeToJson(ListTileNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'type': instance.type, diff --git a/lib/src/api/nodes/list_view_node.g.dart b/lib/src/api/nodes/list_view_node.g.dart index 2dce408d..e7d5491d 100644 --- a/lib/src/api/nodes/list_view_node.g.dart +++ b/lib/src/api/nodes/list_view_node.g.dart @@ -221,6 +221,9 @@ ListViewNode _$ListViewNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..deniedTypes = (json['deniedTypes'] as List) .map((e) => e as String) .toList() @@ -245,7 +248,6 @@ ListViewNode _$ListViewNodeFromJson(Map json) => Map _$ListViewNodeToJson(ListViewNode instance) => { - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -263,6 +265,7 @@ Map _$ListViewNodeToJson(ListViewNode instance) => 'flex': instance.flex, 'aspectRatioLock': instance.aspectRatioLock, 'alignment': instance.alignment.toJson(), + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), 'padding': instance.padding.toJson(), @@ -274,6 +277,7 @@ Map _$ListViewNodeToJson(ListViewNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'deniedTypes': instance.deniedTypes, diff --git a/lib/src/api/nodes/loading_indicator_node.g.dart b/lib/src/api/nodes/loading_indicator_node.g.dart index 6e5dadc3..9c6ad08b 100644 --- a/lib/src/api/nodes/loading_indicator_node.g.dart +++ b/lib/src/api/nodes/loading_indicator_node.g.dart @@ -161,6 +161,9 @@ LoadingIndicatorNode _$LoadingIndicatorNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$LoadingIndicatorNodeToJson( @@ -195,6 +198,7 @@ Map _$LoadingIndicatorNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/navigation_bar_node.g.dart b/lib/src/api/nodes/navigation_bar_node.g.dart index a441b172..6fd9683a 100644 --- a/lib/src/api/nodes/navigation_bar_node.g.dart +++ b/lib/src/api/nodes/navigation_bar_node.g.dart @@ -186,6 +186,9 @@ NavigationBarNode _$NavigationBarNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$NavigationBarNodeToJson(NavigationBarNode instance) => @@ -219,6 +222,7 @@ Map _$NavigationBarNodeToJson(NavigationBarNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'currentIndex': instance.currentIndex, diff --git a/lib/src/api/nodes/page_view_node.g.dart b/lib/src/api/nodes/page_view_node.g.dart index bdc21484..5d4deae0 100644 --- a/lib/src/api/nodes/page_view_node.g.dart +++ b/lib/src/api/nodes/page_view_node.g.dart @@ -195,6 +195,9 @@ PageViewNode _$PageViewNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..ephemeral = json['ephemeral'] as bool? ?? false ..allowedTypes = (json['allowedTypes'] as List) .map((e) => e as String) @@ -240,6 +243,7 @@ Map _$PageViewNodeToJson(PageViewNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'clipsContent': instance.clipsContent, diff --git a/lib/src/api/nodes/placeholder_node.g.dart b/lib/src/api/nodes/placeholder_node.g.dart index da90e37b..5e7b4ae4 100644 --- a/lib/src/api/nodes/placeholder_node.g.dart +++ b/lib/src/api/nodes/placeholder_node.g.dart @@ -140,6 +140,9 @@ PlaceholderNode _$PlaceholderNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$PlaceholderNodeToJson(PlaceholderNode instance) => @@ -173,6 +176,7 @@ Map _$PlaceholderNodeToJson(PlaceholderNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'leadingAsset': ?instance.leadingAsset, diff --git a/lib/src/api/nodes/progressbar_node.g.dart b/lib/src/api/nodes/progressbar_node.g.dart index faaf5a17..c44f4767 100644 --- a/lib/src/api/nodes/progressbar_node.g.dart +++ b/lib/src/api/nodes/progressbar_node.g.dart @@ -159,6 +159,9 @@ ProgressBarNode _$ProgressBarNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$ProgressBarNodeToJson(ProgressBarNode instance) => @@ -192,6 +195,7 @@ Map _$ProgressBarNodeToJson(ProgressBarNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/radio_node.dart b/lib/src/api/nodes/radio_node.dart index a978e43a..cf4a9a55 100644 --- a/lib/src/api/nodes/radio_node.dart +++ b/lib/src/api/nodes/radio_node.dart @@ -97,7 +97,11 @@ class RadioNode extends SceneNode with CustomPropertiesMixin, ScalableMixin { }; @override - List get triggerTypes => [TriggerType.click]; + List get triggerTypes => [ + TriggerType.click, + TriggerType.cursorEnter, + TriggerType.cursorExit, + ]; /// Creates a [RadioNode] instance from a JSON object. factory RadioNode.fromJson(Map json) => _$RadioNodeFromJson(json); diff --git a/lib/src/api/nodes/radio_node.g.dart b/lib/src/api/nodes/radio_node.g.dart index d1ff5b62..4001ef10 100644 --- a/lib/src/api/nodes/radio_node.g.dart +++ b/lib/src/api/nodes/radio_node.g.dart @@ -161,9 +161,13 @@ RadioNode _$RadioNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$RadioNodeToJson(RadioNode instance) => { + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -181,7 +185,6 @@ Map _$RadioNodeToJson(RadioNode instance) => { 'flex': instance.flex, 'aspectRatioLock': instance.aspectRatioLock, 'alignment': instance.alignment.toJson(), - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), 'padding': instance.padding.toJson(), @@ -193,6 +196,7 @@ Map _$RadioNodeToJson(RadioNode instance) => { ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/rectangle_node.dart b/lib/src/api/nodes/rectangle_node.dart index 4875b198..32330af8 100644 --- a/lib/src/api/nodes/rectangle_node.dart +++ b/lib/src/api/nodes/rectangle_node.dart @@ -69,6 +69,8 @@ class RectangleNode extends DefaultShapeNode with CornerMixin, ClipMixin { List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.cursorEnter, + TriggerType.cursorExit, ]; @override diff --git a/lib/src/api/nodes/rectangle_node.g.dart b/lib/src/api/nodes/rectangle_node.g.dart index 3e3fc760..37d0e5d4 100644 --- a/lib/src/api/nodes/rectangle_node.g.dart +++ b/lib/src/api/nodes/rectangle_node.g.dart @@ -209,6 +209,9 @@ RectangleNode _$RectangleNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -218,13 +221,6 @@ RectangleNode _$RectangleNodeFromJson(Map json) => Map _$RectangleNodeToJson(RectangleNode instance) => { - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), - 'variables': instance.variables, - 'multipleVariables': instance.multipleVariables, - 'componentId': ?instance.componentId, - 'markerType': ?_$ComponentMarkerTypeEnumMap[instance.markerType], - 'componentVersion': instance.componentVersion, - 'componentSchema': instance.componentSchema, 'id': instance.id, 'name': instance.name, 'visible': instance.visible, @@ -236,6 +232,13 @@ Map _$RectangleNodeToJson(RectangleNode instance) => 'flex': instance.flex, 'aspectRatioLock': instance.aspectRatioLock, 'alignment': instance.alignment.toJson(), + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), + 'variables': instance.variables, + 'multipleVariables': instance.multipleVariables, + 'componentId': ?instance.componentId, + 'markerType': ?_$ComponentMarkerTypeEnumMap[instance.markerType], + 'componentVersion': instance.componentVersion, + 'componentSchema': instance.componentSchema, 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), 'padding': instance.padding.toJson(), @@ -247,6 +250,7 @@ Map _$RectangleNodeToJson(RectangleNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, diff --git a/lib/src/api/nodes/row_column_node.g.dart b/lib/src/api/nodes/row_column_node.g.dart index 4385e68c..01243e44 100644 --- a/lib/src/api/nodes/row_column_node.g.dart +++ b/lib/src/api/nodes/row_column_node.g.dart @@ -267,6 +267,9 @@ RowColumnNode _$RowColumnNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -306,6 +309,7 @@ Map _$RowColumnNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'opacity': instance.opacity, diff --git a/lib/src/api/nodes/segmented_button_node.g.dart b/lib/src/api/nodes/segmented_button_node.g.dart index 7229d2d8..3c58ec7c 100644 --- a/lib/src/api/nodes/segmented_button_node.g.dart +++ b/lib/src/api/nodes/segmented_button_node.g.dart @@ -177,6 +177,9 @@ SegmentedButtonNode _$SegmentedButtonNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$SegmentedButtonNodeToJson( @@ -211,6 +214,7 @@ Map _$SegmentedButtonNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/signature_node.g.dart b/lib/src/api/nodes/signature_node.g.dart index b64511d5..aa94fbc2 100644 --- a/lib/src/api/nodes/signature_node.g.dart +++ b/lib/src/api/nodes/signature_node.g.dart @@ -33,6 +33,10 @@ Map _$SignatureNodeOmitDefaults( if (isJsonDefault(instance.padding, EdgeInsetsModel.zero)) json.remove('padding'); if (isJsonDefault(instance.rotationDegrees, 0)) json.remove('rotation'); + if (isJsonDefault(instance.interactionStates, {})) + json.remove('interactionStates'); + if (isJsonDefault(instance.tooltip, '')) json.remove('tooltip'); + if (isJsonDefault(instance.zoomable, false)) json.remove('zoomable'); if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); if (isJsonDefault(instance.variables, {})) json.remove('variables'); if (isJsonDefault(instance.multipleVariables, {})) @@ -159,6 +163,22 @@ SignatureNode _$SignatureNodeFromJson(Map json) => {} ..widthFactor = (json['widthFactor'] as num?)?.toDouble() ..heightFactor = (json['heightFactor'] as num?)?.toDouble() + ..interactionStates = + (json['interactionStates'] as Map?)?.map( + (k, e) => MapEntry( + $enumDecode(_$InteractionStateEnumMap, k), + (e as Map).map( + (k, e) => + MapEntry(k as String, Map.from(e as Map)), + ), + ), + ) ?? + {} + ..tooltip = json['tooltip'] as String? ?? '' + ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$SignatureNodeToJson(SignatureNode instance) => @@ -187,6 +207,12 @@ Map _$SignatureNodeToJson(SignatureNode instance) => 'rotation': instance.rotationDegrees, 'widthFactor': ?instance.widthFactor, 'heightFactor': ?instance.heightFactor, + 'interactionStates': instance.interactionStates.map( + (k, e) => MapEntry(_$InteractionStateEnumMap[k]!, e), + ), + 'tooltip': instance.tooltip, + 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), @@ -211,6 +237,14 @@ const _$ComponentMarkerTypeEnumMap = { ComponentMarkerType.instance: 'instance', }; +const _$InteractionStateEnumMap = { + InteractionState.hovered: 'hovered', + InteractionState.focused: 'focused', + InteractionState.pressed: 'pressed', + InteractionState.selected: 'selected', + InteractionState.disabled: 'disabled', +}; + SignatureProperties _$SignaturePropertiesFromJson( Map json, ) => SignatureProperties( diff --git a/lib/src/api/nodes/single_placeholder_node.g.dart b/lib/src/api/nodes/single_placeholder_node.g.dart index d8ce6f51..d35e87f4 100644 --- a/lib/src/api/nodes/single_placeholder_node.g.dart +++ b/lib/src/api/nodes/single_placeholder_node.g.dart @@ -155,6 +155,9 @@ SinglePlaceholderNode _$SinglePlaceholderNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$SinglePlaceholderNodeToJson( @@ -189,6 +192,7 @@ Map _$SinglePlaceholderNodeToJson( ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'children': instance.children, 'type': instance.type, diff --git a/lib/src/api/nodes/slider_node.g.dart b/lib/src/api/nodes/slider_node.g.dart index 93ef55f9..a14de108 100644 --- a/lib/src/api/nodes/slider_node.g.dart +++ b/lib/src/api/nodes/slider_node.g.dart @@ -197,6 +197,9 @@ SliderNode _$SliderNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$SliderNodeToJson(SliderNode instance) => @@ -230,6 +233,7 @@ Map _$SliderNodeToJson(SliderNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'value': instance.value, diff --git a/lib/src/api/nodes/spacer_node.g.dart b/lib/src/api/nodes/spacer_node.g.dart index 9d7b529c..fd2f5904 100644 --- a/lib/src/api/nodes/spacer_node.g.dart +++ b/lib/src/api/nodes/spacer_node.g.dart @@ -110,6 +110,9 @@ SpacerNode _$SpacerNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$SpacerNodeToJson(SpacerNode instance) => @@ -139,6 +142,7 @@ Map _$SpacerNodeToJson(SpacerNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, }; diff --git a/lib/src/api/nodes/switch_node.g.dart b/lib/src/api/nodes/switch_node.g.dart index 3c34fb4e..229363ab 100644 --- a/lib/src/api/nodes/switch_node.g.dart +++ b/lib/src/api/nodes/switch_node.g.dart @@ -170,6 +170,9 @@ SwitchNode _$SwitchNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$SwitchNodeToJson(SwitchNode instance) => @@ -203,6 +206,7 @@ Map _$SwitchNodeToJson(SwitchNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'value': instance.value, diff --git a/lib/src/api/nodes/tab_bar_node.g.dart b/lib/src/api/nodes/tab_bar_node.g.dart index b93405cc..cc310976 100644 --- a/lib/src/api/nodes/tab_bar_node.g.dart +++ b/lib/src/api/nodes/tab_bar_node.g.dart @@ -205,6 +205,9 @@ TabBarNode _$TabBarNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..scrollDirection = $enumDecode(_$AxisCEnumMap, json['scrollDirection']) ..reverse = json['reverse'] as bool ..primary = json['primary'] as bool @@ -246,6 +249,7 @@ Map _$TabBarNodeToJson(TabBarNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'isScrollable': instance.isScrollable, 'scrollDirection': _$AxisCEnumMap[instance.scrollDirection]!, diff --git a/lib/src/api/nodes/text_field_node.g.dart b/lib/src/api/nodes/text_field_node.g.dart index 1ca7bd8b..641b5e81 100644 --- a/lib/src/api/nodes/text_field_node.g.dart +++ b/lib/src/api/nodes/text_field_node.g.dart @@ -194,6 +194,9 @@ TextFieldNode _$TextFieldNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$TextFieldNodeToJson(TextFieldNode instance) => @@ -227,6 +230,7 @@ Map _$TextFieldNodeToJson(TextFieldNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/text_node.dart b/lib/src/api/nodes/text_node.dart index 520bdaa1..fb8d6db1 100644 --- a/lib/src/api/nodes/text_node.dart +++ b/lib/src/api/nodes/text_node.dart @@ -148,6 +148,8 @@ class TextNode extends SceneNode with BlendMixin, TextMixin, FontMixin { List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.cursorEnter, + TriggerType.cursorExit, ]; @override diff --git a/lib/src/api/nodes/text_node.g.dart b/lib/src/api/nodes/text_node.g.dart index 01949d07..c3a2c6b9 100644 --- a/lib/src/api/nodes/text_node.g.dart +++ b/lib/src/api/nodes/text_node.g.dart @@ -216,6 +216,9 @@ TextNode _$TextNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$TextNodeToJson(TextNode instance) => { @@ -260,6 +263,7 @@ Map _$TextNodeToJson(TextNode instance) => { ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, diff --git a/lib/src/api/nodes/variance_node.g.dart b/lib/src/api/nodes/variance_node.g.dart index be7a515d..58482916 100644 --- a/lib/src/api/nodes/variance_node.g.dart +++ b/lib/src/api/nodes/variance_node.g.dart @@ -168,6 +168,9 @@ VarianceNode _$VarianceNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..ephemeral = json['ephemeral'] as bool? ?? false ..allowedTypes = (json['allowedTypes'] as List) .map((e) => e as String) @@ -208,6 +211,7 @@ Map _$VarianceNodeToJson(VarianceNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'ephemeral': instance.ephemeral, 'allowedTypes': instance.allowedTypes, diff --git a/lib/src/api/nodes/web_view_node.g.dart b/lib/src/api/nodes/web_view_node.g.dart index 48c3c29e..7d0febff 100644 --- a/lib/src/api/nodes/web_view_node.g.dart +++ b/lib/src/api/nodes/web_view_node.g.dart @@ -151,6 +151,9 @@ WebViewNode _$WebViewNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..loopAnimation = json['loopAnimation'] == null + ? null + : LoopAnimation.fromJson(json['loopAnimation'] as Map) ..type = json['type'] as String; Map _$WebViewNodeToJson(WebViewNode instance) => @@ -184,6 +187,7 @@ Map _$WebViewNodeToJson(WebViewNode instance) => ), 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, + 'loopAnimation': ?instance.loopAnimation?.toJson(), 'enabled': instance.enabled, 'type': instance.type, 'properties': instance.properties.toJson(), diff --git a/test/loop_animation_test.dart b/test/loop_animation_test.dart new file mode 100644 index 00000000..b2d3ca8f --- /dev/null +++ b/test/loop_animation_test.dart @@ -0,0 +1,157 @@ +import 'dart:convert'; + +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +Map encodeDecode(Map json) => jsonDecode(jsonEncode(json)) as Map; + +void main() { + group('LoopAnimation', () { + test('a pulsing halo round-trips through JSON on any node', () { + final Map json = { + 'type': 'rectangle', + 'id': 'halo', + 'name': 'Halo', + 'basicBoxLocal': NodeBox(0, 0, 58, 58).toJson(), + 'loopAnimation': { + 'type': 'pulse', + 'durationMs': 1400, + 'scaleFrom': 0.655, + 'scaleTo': 1.0, + 'opacityFrom': 0.35, + 'opacityTo': 0.0, + 'curve': 'easeOut', + }, + 'variables': {'loopAnimationEnabled': r'${selectedZone == "cab"}'}, + }; + final BaseNode node = NodeJsonConverter().fromJson(json)!; + expect( + node.loopAnimation, + const LoopAnimation( + type: LoopAnimationType.pulse, + durationMs: 1400, + scaleFrom: 0.655, + scaleTo: 1, + opacityFrom: 0.35, + opacityTo: 0, + curve: LoopAnimationCurve.easeOut, + ), + ); + + final Map written = encodeDecode(node.toJson()); + expect(written['loopAnimation'], { + 'type': 'pulse', + 'durationMs': 1400, + 'curve': 'easeOut', + 'scaleFrom': 0.655, + 'opacityFrom': 0.35, + 'opacityTo': 0.0, + }); + final BaseNode read = NodeJsonConverter().fromJson(written)!; + expect(read.loopAnimation, node.loopAnimation); + expect(read.variables['loopAnimationEnabled'], isNotNull); + }); + + test('nodes without an animation write no key', () { + for (final BaseNode node in [ + RectangleNode(id: 'r', name: 'R', basicBoxLocal: NodeBox(0, 0, 1, 1)), + RowColumnNode( + id: 'c', + name: 'C', + basicBoxLocal: NodeBox(0, 0, 1, 1), + rowColumnType: RowColumnType.column, + children: [], + ), + IconNode(id: 'i', name: 'I', basicBoxLocal: NodeBox(0, 0, 24, 24)), + ]) { + expect(node.toJson().containsKey('loopAnimation'), isFalse); + final BaseNode read = NodeJsonConverter().fromJson( + encodeDecode(node.toJson()), + )!; + expect(read.loopAnimation, isNull); + } + }); + + test('every type and its preset round-trip', () { + for (final LoopAnimationType type in LoopAnimationType.values) { + final LoopAnimation animation = LoopAnimation.preset(type) + .copyWith(delayMs: 200, enabled: false, turns: -2); + expect( + LoopAnimation.fromJson(encodeDecode(animation.toJson())), + animation, + ); + } + // Defaults are left out. + expect(const LoopAnimation(type: LoopAnimationType.spin).toJson(), { + 'type': 'spin', + }); + }); + + test('unknown values fall back instead of throwing', () { + expect( + LoopAnimation.fromJson({'type': 'wobble', 'curve': 'bounce'}), + const LoopAnimation(type: LoopAnimationType.pulse), + ); + }); + + test('loopAnimationEnabled is a bindable field', () { + final RectangleNode node = RectangleNode( + id: 'r', + name: 'R', + basicBoxLocal: NodeBox(0, 0, 1, 1), + )..loopAnimation = LoopAnimation.preset(LoopAnimationType.fade); + final field = node.fields['loopAnimationEnabled']!; + expect(field.getValue(), isTrue); + field.setValue(false); + expect(node.loopAnimation!.enabled, isFalse); + }); + }); + + group('Cursor triggers', () { + test('nodes with a click trigger offer cursor enter and exit', () { + final List nodes = [ + RectangleNode(id: 'r', name: 'R', basicBoxLocal: NodeBox(0, 0, 1, 1)), + FrameNode( + id: 'f', + name: 'F', + basicBoxLocal: NodeBox(0, 0, 1, 1), + children: [], + ), + TextNode( + id: 't', + name: 'T', + basicBoxLocal: NodeBox(0, 0, 1, 1), + characters: 'Hi', + ), + ]; + for (final ReactionMixin node in nodes) { + expect(node.triggerTypes.first, TriggerType.click); + expect( + node.triggerTypes, + containsAll([TriggerType.cursorEnter, TriggerType.cursorExit]), + ); + } + }); + + test('cursor reactions round-trip', () { + final RectangleNode node = + RectangleNode(id: 'r', name: 'R', basicBoxLocal: NodeBox(0, 0, 1, 1)) + ..reactions = [ + Reaction( + trigger: TriggerModel(type: TriggerType.cursorEnter), + action: SetVariableAction( + variable: VariableData(id: 'h', name: 'hoveredId'), + newValue: r'${item.id}', + ), + ), + ]; + final BaseNode read = NodeJsonConverter().fromJson( + encodeDecode(node.toJson()), + )!; + expect( + (read as ReactionMixin).reactions.single.trigger.type, + TriggerType.cursorEnter, + ); + }); + }); +} From 693ec79a19917d1bfb08269c7bf96fb74ca05589 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 22:40:26 +0000 Subject: [PATCH 04/13] Regenerate SignatureNode with loopAnimation; cover it in the JSON defaults test Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011WedhUVWdWR5swqZYMuGfP --- test/json_defaults_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/test/json_defaults_test.dart b/test/json_defaults_test.dart index bb2e1328..86b91045 100644 --- a/test/json_defaults_test.dart +++ b/test/json_defaults_test.dart @@ -169,6 +169,7 @@ List sampleNodes() { BadgeNode.empty(), ButtonNode.empty(), CheckboxNode.empty(), + SignatureNode.empty(), ChipNode.empty(), DividerNode.empty(), EmbeddedVideoNode.empty(), From 904e3351ec34f315953da930874a65e8f92b9596 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 23:04:11 +0000 Subject: [PATCH 05/13] Add bottom sheet and side sheet presentations to ShowDialogAction ShowDialogAction gains presentation (dialog | bottomSheet | sideSheet, default dialog), sheetWidth, maxHeightFactor (0.92) and showDragHandle (true). Defaults are omitted from JSON, so saved actions are unchanged. copyWith now keeps nonBlocking and enabled. Co-Authored-By: Claude Opus 5.5 --- .../api/models/action/show_dialog_action.dart | 61 ++++++++++++++++- .../models/action/show_dialog_action.g.dart | 26 ++++++++ test/show_dialog_action_test.dart | 66 +++++++++++++++++++ 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 test/show_dialog_action_test.dart diff --git a/lib/src/api/models/action/show_dialog_action.dart b/lib/src/api/models/action/show_dialog_action.dart index 80bfdd32..639e3f47 100644 --- a/lib/src/api/models/action/show_dialog_action.dart +++ b/lib/src/api/models/action/show_dialog_action.dart @@ -6,7 +6,31 @@ import '../models.dart'; part 'show_dialog_action.g.dart'; -/// An action that navigates to a new page. +/// How a [ShowDialogAction] presents its destination canvas. +enum DialogPresentation { + /// Centered over the page at the canvas's design size. + dialog, + + /// Anchored to the bottom edge, as wide as the screen and as tall as the + /// canvas's content, up to [ShowDialogAction.maxHeightFactor] of the + /// screen. Slides up from the bottom. + bottomSheet, + + /// Anchored to the right edge, as tall as the screen and + /// [ShowDialogAction.sheetWidth] wide. Slides in from the right. + sideSheet; + + /// Displayable name. + String get prettify => switch (this) { + DialogPresentation.dialog => 'Dialog', + DialogPresentation.bottomSheet => 'Bottom Sheet', + DialogPresentation.sideSheet => 'Side Sheet', + }; +} + +/// An action that shows a canvas over the current page, as a centered +/// dialog, a bottom sheet or a side sheet (see [presentation]). A +/// "Navigate back" action in the canvas closes it. @JsonSerializable() class ShowDialogAction extends ActionModel with Equatable, SerializableMixin { /// Destination canvas node's ID. @@ -24,6 +48,22 @@ class ShowDialogAction extends ActionModel with Equatable, SerializableMixin { /// Whether to show a close button on the dialog. final bool showCloseButton; + /// How the canvas is presented. + @JsonKey(unknownEnumValue: DialogPresentation.dialog) + final DialogPresentation presentation; + + /// Width of a [DialogPresentation.sideSheet], in logical pixels. Null takes + /// the destination canvas's width. + final double? sheetWidth; + + /// The most of the screen's height a [DialogPresentation.bottomSheet] takes, + /// from 0 to 1. A taller canvas scrolls within it. + final double maxHeightFactor; + + /// Whether a [DialogPresentation.bottomSheet] shows a grab handle at its + /// top. + final bool showDragHandle; + /// Creates a new [ShowDialogAction]. ShowDialogAction({ required this.destinationId, @@ -31,6 +71,10 @@ class ShowDialogAction extends ActionModel with Equatable, SerializableMixin { this.barrierColor = ColorRGBA.black54, this.barrierDismissible = true, this.showCloseButton = false, + this.presentation = DialogPresentation.dialog, + this.sheetWidth, + this.maxHeightFactor = 0.92, + this.showDragHandle = true, super.nonBlocking, super.enabled, }) : params = {...params ?? {}}, @@ -43,6 +87,10 @@ class ShowDialogAction extends ActionModel with Equatable, SerializableMixin { barrierDismissible, barrierColor, showCloseButton, + presentation, + sheetWidth, + maxHeightFactor, + showDragHandle, ]; /// Duplicates this [ShowDialogAction] with given data overrides. @@ -52,12 +100,23 @@ class ShowDialogAction extends ActionModel with Equatable, SerializableMixin { bool? barrierDismissible, ColorRGBA? barrierColor, bool? showCloseButton, + DialogPresentation? presentation, + double? sheetWidth, + bool clearSheetWidth = false, + double? maxHeightFactor, + bool? showDragHandle, }) => ShowDialogAction( destinationId: destinationId ?? this.destinationId, params: {...this.params, ...params ?? {}}, barrierDismissible: barrierDismissible ?? this.barrierDismissible, barrierColor: barrierColor ?? this.barrierColor, showCloseButton: showCloseButton ?? this.showCloseButton, + presentation: presentation ?? this.presentation, + sheetWidth: clearSheetWidth ? null : sheetWidth ?? this.sheetWidth, + maxHeightFactor: maxHeightFactor ?? this.maxHeightFactor, + showDragHandle: showDragHandle ?? this.showDragHandle, + nonBlocking: nonBlocking, + enabled: enabled, ); /// Creates a new [ShowDialogAction] instance from a JSON data. diff --git a/lib/src/api/models/action/show_dialog_action.g.dart b/lib/src/api/models/action/show_dialog_action.g.dart index 4d8db00c..756f4845 100644 --- a/lib/src/api/models/action/show_dialog_action.g.dart +++ b/lib/src/api/models/action/show_dialog_action.g.dart @@ -16,6 +16,12 @@ Map _$ShowDialogActionOmitDefaults( json.remove('barrierColor'); if (isJsonDefault(instance.showCloseButton, false)) json.remove('showCloseButton'); + if (isJsonDefault(instance.presentation, DialogPresentation.dialog)) + json.remove('presentation'); + if (isJsonDefault(instance.maxHeightFactor, 0.92)) + json.remove('maxHeightFactor'); + if (isJsonDefault(instance.showDragHandle, true)) + json.remove('showDragHandle'); if (isJsonDefault(instance.nonBlocking, false)) json.remove('nonBlocking'); if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); return json; @@ -35,6 +41,16 @@ ShowDialogAction _$ShowDialogActionFromJson(Map json) => ShowDialogAction( : ColorRGBA.fromJson(json['barrierColor']), barrierDismissible: json['barrierDismissible'] as bool? ?? true, showCloseButton: json['showCloseButton'] as bool? ?? false, + presentation: + $enumDecodeNullable( + _$DialogPresentationEnumMap, + json['presentation'], + unknownValue: DialogPresentation.dialog, + ) ?? + DialogPresentation.dialog, + sheetWidth: (json['sheetWidth'] as num?)?.toDouble(), + maxHeightFactor: (json['maxHeightFactor'] as num?)?.toDouble() ?? 0.92, + showDragHandle: json['showDragHandle'] as bool? ?? true, nonBlocking: json['nonBlocking'] as bool? ?? false, enabled: json['enabled'] as bool? ?? true, )..type = $enumDecode(_$ActionTypeEnumMap, json['type']); @@ -49,8 +65,18 @@ Map _$ShowDialogActionToJson(ShowDialogAction instance) => 'barrierDismissible': instance.barrierDismissible, 'barrierColor': ?instance.barrierColor?.toJson(), 'showCloseButton': instance.showCloseButton, + 'presentation': _$DialogPresentationEnumMap[instance.presentation]!, + 'sheetWidth': ?instance.sheetWidth, + 'maxHeightFactor': instance.maxHeightFactor, + 'showDragHandle': instance.showDragHandle, }; +const _$DialogPresentationEnumMap = { + DialogPresentation.dialog: 'dialog', + DialogPresentation.bottomSheet: 'bottomSheet', + DialogPresentation.sideSheet: 'sideSheet', +}; + const _$ActionTypeEnumMap = { ActionType.navigation: 'navigation', ActionType.showDialog: 'showDialog', diff --git a/test/show_dialog_action_test.dart b/test/show_dialog_action_test.dart new file mode 100644 index 00000000..048cc0f7 --- /dev/null +++ b/test/show_dialog_action_test.dart @@ -0,0 +1,66 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('A saved ShowDialogAction stays a dialog and omits defaults', () { + final action = ActionModel.fromJson({ + 'type': 'showDialog', + 'destinationId': 'canvas-1', + 'params': {'id': r'${data.id}'}, + }) as ShowDialogAction; + expect(action.presentation, DialogPresentation.dialog); + expect(action.sheetWidth, isNull); + expect(action.maxHeightFactor, 0.92); + expect(action.showDragHandle, isTrue); + expect(action.toJson(), { + 'type': 'showDialog', + 'destinationId': 'canvas-1', + 'params': {'id': r'${data.id}'}, + }); + }); + + test('ShowDialogAction round-trips a bottom sheet', () { + final json = { + 'type': 'showDialog', + 'destinationId': 'account', + 'params': {}, + 'presentation': 'bottomSheet', + 'maxHeightFactor': 0.8, + 'showDragHandle': false, + }; + final action = ActionModel.fromJson(json) as ShowDialogAction; + expect(action.presentation, DialogPresentation.bottomSheet); + expect(action.maxHeightFactor, 0.8); + expect(action.showDragHandle, isFalse); + expect(action.toJson(), json); + expect(ActionModel.fromJson(action.toJson()), action); + }); + + test('ShowDialogAction round-trips a side sheet', () { + final action = ShowDialogAction( + destinationId: 'flag-defect', + presentation: DialogPresentation.sideSheet, + sheetWidth: 480, + barrierDismissible: false, + ); + final Map json = action.toJson(); + expect(json['presentation'], 'sideSheet'); + expect(json['sheetWidth'], 480); + expect(json.containsKey('maxHeightFactor'), isFalse); + expect(ActionModel.fromJson(json), action); + + final cleared = action.copyWith(clearSheetWidth: true); + expect(cleared.sheetWidth, isNull); + expect(cleared.presentation, DialogPresentation.sideSheet); + }); + + test('An unknown presentation falls back to a dialog', () { + final action = ActionModel.fromJson({ + 'type': 'showDialog', + 'destinationId': 'x', + 'presentation': 'popover', + }) as ShowDialogAction; + expect(action.presentation, DialogPresentation.dialog); + expect(DialogPresentation.bottomSheet.prettify, 'Bottom Sheet'); + }); +} From 210ec262d7436b6b739154cc60608953632bd1ec Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 03:59:50 +0000 Subject: [PATCH 06/13] Spacer and grid JSON defaults; grid rows sized to their content SpacerNode passed none of positioningMode, aspectRatioLock and reactions to SceneNode, so json_serializable cast them as required and fromJson refused a spacer without them. GridViewNode set isScrollable and useFlutterListView in its body, so fromJson required both. They are now constructor parameters with defaults; toJson still writes them because older readers require them. GridDelegateProperties.shrinkWrapRows makes each row as tall as its tallest item; the grid then leaves its item's main axis to the item's fit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011WedhUVWdWR5swqZYMuGfP --- CHANGELOG.md | 12 ++++ lib/src/api/nodes/grid_view_node.dart | 57 +++++++++++++++--- lib/src/api/nodes/grid_view_node.g.dart | 15 ++++- lib/src/api/nodes/spacer_node.dart | 19 +++++- lib/src/api/nodes/spacer_node.g.dart | 35 ++++++----- test/json_defaults_test.dart | 78 +++++++++++++++++++++++++ 6 files changed, 193 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c94ed45..31bc01ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ repeated or mirror; default clamp, left out of the JSON) and `PaintModel.gradientExtent`, the length in pixels of one run of the gradient. A short repeating gradient with hard stops paints stripes. +- `GridDelegateProperties.shrinkWrapRows`: each row of a grid is as tall + as its tallest item (like a `Row` in an `IntrinsicHeight`) instead of a + fixed `mainAxisExtent` or `childAspectRatio`. Default false, left out of + the JSON. + +### Fixes + +- `SpacerNode.fromJson` defaults `positioningMode` (align), + `aspectRatioLock` (false) and `reactions` ([]) like every other node, and + `GridViewNode.fromJson` defaults `isScrollable` (true) and + `useFlutterListView` (false). Both still write those keys for older + readers. ## 6.1.0 diff --git a/lib/src/api/nodes/grid_view_node.dart b/lib/src/api/nodes/grid_view_node.dart index 626d8b38..f51d20a0 100644 --- a/lib/src/api/nodes/grid_view_node.dart +++ b/lib/src/api/nodes/grid_view_node.dart @@ -68,6 +68,11 @@ class GridViewNode extends SinglePlaceholderNode bool clipsContent = true, this.statusVariable, + // A grid always scrolls its items (see [isScrollingEnforced]) and never + // uses a Flutter ListView, so neither has to be in its JSON. + bool isScrollable = true, + bool useFlutterListView = false, + // [CloudDatabaseMixin] properties. bool useCloudDatabase = false, String? collectionPath, @@ -93,14 +98,14 @@ class GridViewNode extends SinglePlaceholderNode setClipMixin(clipsContent: clipsContent); setScrollableMixin( - isScrollable: true, + isScrollable: isScrollable, scrollDirection: scrollDirection, reverse: reverse, physics: physics, primary: primary, shrinkWrap: false, keyboardDismissBehavior: keyboardDismissBehavior, - useFlutterListView: false, + useFlutterListView: useFlutterListView, shouldAlwaysScroll: shouldAlwaysScroll, ); @@ -169,18 +174,20 @@ class GridViewNode extends SinglePlaceholderNode childCrossAxisExtent, ); + // Rows sized to their content leave the item's main axis to its own fit. + final bool shrinkWrapRows = properties.gridDelegate.shrinkWrapRows; return switch (scrollDirection) { AxisC.horizontal => BoxConstraintsModel( - minWidth: childMainAxisExtent, - maxWidth: childMainAxisExtent, + minWidth: shrinkWrapRows ? null : childMainAxisExtent, + maxWidth: shrinkWrapRows ? null : childMainAxisExtent, minHeight: childCrossAxisExtent, maxHeight: childCrossAxisExtent, ), AxisC.vertical => BoxConstraintsModel( minWidth: childCrossAxisExtent, maxWidth: childCrossAxisExtent, - minHeight: childMainAxisExtent, - maxHeight: childMainAxisExtent, + minHeight: shrinkWrapRows ? null : childMainAxisExtent, + maxHeight: shrinkWrapRows ? null : childMainAxisExtent, ), }; } @@ -210,8 +217,21 @@ class GridViewNode extends SinglePlaceholderNode /// Creates a [GridViewNode] from a JSON object. factory GridViewNode.fromJson(Map json) => _$GridViewNodeFromJson(json); + /// Keys that SDKs up to codelessly_api 6.0.0 require in a grid's JSON. They are written + /// even at their defaults so those SDKs can still read it. + static const List _legacyRequiredKeys = [ + 'isScrollable', + 'useFlutterListView', + ]; + @override - Map toJson() => _$GridViewNodeOmitDefaults(this, _$GridViewNodeToJson(this)); + Map toJson() { + final Map json = _$GridViewNodeToJson(this); + final Map legacy = { + for (final key in _legacyRequiredKeys) key: json[key], + }; + return {..._$GridViewNodeOmitDefaults(this, json), ...legacy}; + } } /// The properties of a [GridViewNode]. @@ -301,6 +321,13 @@ sealed class GridDelegateProperties /// [childAspectRatio] controls the main axis extent of each child. double? mainAxisExtent; + /// Whether each row (each column of a horizontal grid) takes the main axis + /// extent of its largest item instead of a fixed one, like a [Row] of + /// cells in an `IntrinsicHeight`: items keep their own main axis fit and + /// the other cells of the row stretch to match. [mainAxisExtent] and + /// [childAspectRatio] are then ignored. + bool shrinkWrapRows; + /// Creates a new [GridDelegateProperties] instance. GridDelegateProperties( this.type, { @@ -308,6 +335,7 @@ sealed class GridDelegateProperties this.crossAxisSpacing = 0, this.childAspectRatio = 1, this.mainAxisExtent, + this.shrinkWrapRows = false, }); /// Creates a copy of this [GridDelegateProperties] instance with the given @@ -318,6 +346,7 @@ sealed class GridDelegateProperties double? childAspectRatio, double? mainAxisExtent, bool forceMainAxisExtent = false, + bool? shrinkWrapRows, }); factory GridDelegateProperties.fromJson(Map json) { @@ -337,6 +366,7 @@ sealed class GridDelegateProperties crossAxisSpacing, childAspectRatio, mainAxisExtent, + shrinkWrapRows, ]; @override @@ -370,6 +400,13 @@ sealed class GridDelegateProperties (value) => mainAxisExtent = value, requiresLayout: true, ), + 'shrinkWrapRows': BoolFieldAccess( + 'Shrink-wrap Rows', + 'Whether each row is as tall as its tallest item.', + () => shrinkWrapRows, + (value) => shrinkWrapRows = value, + requiresLayout: true, + ), }; } @@ -387,6 +424,7 @@ class FixedCrossAxisCountGridDelegateProperties extends GridDelegateProperties { super.crossAxisSpacing = 0, super.childAspectRatio = 1, super.mainAxisExtent, + super.shrinkWrapRows = false, }) : super(GridDelegateType.fixedCrossAxisCount); @override @@ -409,6 +447,7 @@ class FixedCrossAxisCountGridDelegateProperties extends GridDelegateProperties { double? childAspectRatio, double? mainAxisExtent, bool forceMainAxisExtent = false, + bool? shrinkWrapRows, }) { return FixedCrossAxisCountGridDelegateProperties( crossAxisCount: crossAxisCount ?? this.crossAxisCount, @@ -418,6 +457,7 @@ class FixedCrossAxisCountGridDelegateProperties extends GridDelegateProperties { mainAxisExtent: forceMainAxisExtent ? mainAxisExtent : mainAxisExtent ?? super.mainAxisExtent, + shrinkWrapRows: shrinkWrapRows ?? super.shrinkWrapRows, ); } @@ -451,6 +491,7 @@ class MaxCrossAxisExtentGridDelegateProperties extends GridDelegateProperties { super.crossAxisSpacing = 0, super.childAspectRatio = 1, super.mainAxisExtent, + super.shrinkWrapRows = false, }) : super(GridDelegateType.maxCrossAxisExtent); @override @@ -473,6 +514,7 @@ class MaxCrossAxisExtentGridDelegateProperties extends GridDelegateProperties { double? childAspectRatio, double? mainAxisExtent, bool forceMainAxisExtent = false, + bool? shrinkWrapRows, }) { return MaxCrossAxisExtentGridDelegateProperties( maxCrossAxisExtent: maxCrossAxisExtent ?? this.maxCrossAxisExtent, @@ -482,6 +524,7 @@ class MaxCrossAxisExtentGridDelegateProperties extends GridDelegateProperties { mainAxisExtent: forceMainAxisExtent ? mainAxisExtent : mainAxisExtent ?? super.mainAxisExtent, + shrinkWrapRows: shrinkWrapRows ?? super.shrinkWrapRows, ); } diff --git a/lib/src/api/nodes/grid_view_node.g.dart b/lib/src/api/nodes/grid_view_node.g.dart index 669a815f..c62c130f 100644 --- a/lib/src/api/nodes/grid_view_node.g.dart +++ b/lib/src/api/nodes/grid_view_node.g.dart @@ -47,6 +47,7 @@ Map _$GridViewNodeOmitDefaults( json.remove('componentSchema'); if (isJsonDefault(instance.reactions, const [])) json.remove('reactions'); if (isJsonDefault(instance.children, [])) json.remove('children'); + if (isJsonDefault(instance.isScrollable, true)) json.remove('isScrollable'); if (isJsonDefault(instance.scrollDirection, AxisC.vertical)) json.remove('scrollDirection'); if (isJsonDefault(instance.reverse, false)) json.remove('reverse'); @@ -58,6 +59,8 @@ Map _$GridViewNodeOmitDefaults( ScrollViewKeyboardDismissBehaviorC.manual, )) json.remove('keyboardDismissBehavior'); + if (isJsonDefault(instance.useFlutterListView, false)) + json.remove('useFlutterListView'); if (isJsonDefault(instance.shouldAlwaysScroll, true)) json.remove('shouldAlwaysScroll'); if (isJsonDefault(instance.clipsContent, true)) json.remove('clipsContent'); @@ -88,6 +91,8 @@ Map _$FixedCrossAxisCountGridDelegatePropertiesOmitDefaults( json.remove('crossAxisSpacing'); if (isJsonDefault(instance.childAspectRatio, 1)) json.remove('childAspectRatio'); + if (isJsonDefault(instance.shrinkWrapRows, false)) + json.remove('shrinkWrapRows'); return json; } @@ -101,6 +106,8 @@ Map _$MaxCrossAxisExtentGridDelegatePropertiesOmitDefaults( json.remove('crossAxisSpacing'); if (isJsonDefault(instance.childAspectRatio, 1)) json.remove('childAspectRatio'); + if (isJsonDefault(instance.shrinkWrapRows, false)) + json.remove('shrinkWrapRows'); return json; } @@ -179,6 +186,8 @@ GridViewNode _$GridViewNodeFromJson(Map json) => : VariableData.fromJson( Map.from(json['statusVariable'] as Map), ), + isScrollable: json['isScrollable'] as bool? ?? true, + useFlutterListView: json['useFlutterListView'] as bool? ?? false, useCloudDatabase: json['useCloudDatabase'] as bool? ?? false, collectionPath: json['collectionPath'] as String?, limit: (json['limit'] as num?)?.toInt() ?? 20, @@ -255,8 +264,6 @@ GridViewNode _$GridViewNodeFromJson(Map json) => ..allowedTypes = (json['allowedTypes'] as List) .map((e) => e as String) .toList() - ..isScrollable = json['isScrollable'] as bool - ..useFlutterListView = json['useFlutterListView'] as bool ..paging = $enumDecodeNullable(_$QueryPagingEnumMap, json['paging']) ?? QueryPaging.scroll @@ -398,6 +405,7 @@ _$FixedCrossAxisCountGridDelegatePropertiesFromJson(Map json) => crossAxisSpacing: (json['crossAxisSpacing'] as num?)?.toDouble() ?? 0, childAspectRatio: (json['childAspectRatio'] as num?)?.toDouble() ?? 1, mainAxisExtent: (json['mainAxisExtent'] as num?)?.toDouble(), + shrinkWrapRows: json['shrinkWrapRows'] as bool? ?? false, ); Map _$FixedCrossAxisCountGridDelegatePropertiesToJson( @@ -407,6 +415,7 @@ Map _$FixedCrossAxisCountGridDelegatePropertiesToJson( 'crossAxisSpacing': instance.crossAxisSpacing, 'childAspectRatio': instance.childAspectRatio, 'mainAxisExtent': ?instance.mainAxisExtent, + 'shrinkWrapRows': instance.shrinkWrapRows, 'crossAxisCount': instance.crossAxisCount, }; @@ -418,6 +427,7 @@ _$MaxCrossAxisExtentGridDelegatePropertiesFromJson(Map json) => crossAxisSpacing: (json['crossAxisSpacing'] as num?)?.toDouble() ?? 0, childAspectRatio: (json['childAspectRatio'] as num?)?.toDouble() ?? 1, mainAxisExtent: (json['mainAxisExtent'] as num?)?.toDouble(), + shrinkWrapRows: json['shrinkWrapRows'] as bool? ?? false, ); Map _$MaxCrossAxisExtentGridDelegatePropertiesToJson( @@ -427,5 +437,6 @@ Map _$MaxCrossAxisExtentGridDelegatePropertiesToJson( 'crossAxisSpacing': instance.crossAxisSpacing, 'childAspectRatio': instance.childAspectRatio, 'mainAxisExtent': ?instance.mainAxisExtent, + 'shrinkWrapRows': instance.shrinkWrapRows, 'maxCrossAxisExtent': instance.maxCrossAxisExtent, }; diff --git a/lib/src/api/nodes/spacer_node.dart b/lib/src/api/nodes/spacer_node.dart index 7310bb09..fc1d680b 100644 --- a/lib/src/api/nodes/spacer_node.dart +++ b/lib/src/api/nodes/spacer_node.dart @@ -41,11 +41,28 @@ class SpacerNode extends SceneNode { super.edgePins, super.parentID, super.constraints, + super.positioningMode, + super.aspectRatioLock, + super.reactions, }); /// Creates a new [SpacerNode] from a JSON data. factory SpacerNode.fromJson(Map json) => _$SpacerNodeFromJson(json); + /// Keys that SDKs up to codelessly_api 6.0.0 require in a spacer's JSON. They are written + /// even at their defaults so those SDKs can still read it. + static const List _legacyRequiredKeys = [ + 'positioningMode', + 'aspectRatioLock', + 'reactions', + ]; + @override - Map toJson() => _$SpacerNodeOmitDefaults(this, _$SpacerNodeToJson(this)); + Map toJson() { + final Map json = _$SpacerNodeToJson(this); + final Map legacy = { + for (final key in _legacyRequiredKeys) key: json[key], + }; + return {..._$SpacerNodeOmitDefaults(this, json), ...legacy}; + } } diff --git a/lib/src/api/nodes/spacer_node.g.dart b/lib/src/api/nodes/spacer_node.g.dart index fd2f5904..3dd91981 100644 --- a/lib/src/api/nodes/spacer_node.g.dart +++ b/lib/src/api/nodes/spacer_node.g.dart @@ -16,11 +16,15 @@ Map _$SpacerNodeOmitDefaults( json.remove('constraints'); if (isJsonDefault(instance.edgePins, EdgePinsModel.standard)) json.remove('edgePins'); + if (isJsonDefault(instance.positioningMode, PositioningMode.align)) + json.remove('positioningMode'); if (isJsonDefault(instance.horizontalFit, SizeFit.fixed)) json.remove('horizontalFit'); if (isJsonDefault(instance.verticalFit, SizeFit.fixed)) json.remove('verticalFit'); if (isJsonDefault(instance.flex, 1)) json.remove('flex'); + if (isJsonDefault(instance.aspectRatioLock, false)) + json.remove('aspectRatioLock'); if (isJsonDefault(instance.interactionStates, {})) json.remove('interactionStates'); if (isJsonDefault(instance.tooltip, '')) json.remove('tooltip'); @@ -33,6 +37,7 @@ Map _$SpacerNodeOmitDefaults( json.remove('componentVersion'); if (isJsonDefault(instance.componentSchema, {})) json.remove('componentSchema'); + if (isJsonDefault(instance.reactions, const [])) json.remove('reactions'); return json; } @@ -62,6 +67,18 @@ SpacerNode _$SpacerNodeFromJson(Map json) => constraints: json['constraints'] == null ? const BoxConstraintsModel() : BoxConstraintsModel.fromJson(json['constraints'] as Map), + positioningMode: + $enumDecodeNullable( + _$PositioningModeEnumMap, + json['positioningMode'], + ) ?? + PositioningMode.align, + aspectRatioLock: json['aspectRatioLock'] as bool? ?? false, + reactions: + (json['reactions'] as List?) + ?.map((e) => Reaction.fromJson(e as Map)) + .toList() ?? + const [], ) ..variables = (json['variables'] as Map?)?.map( @@ -87,14 +104,6 @@ SpacerNode _$SpacerNodeFromJson(Map json) => (k, e) => MapEntry(k as String, e), ) ?? {} - ..positioningMode = $enumDecode( - _$PositioningModeEnumMap, - json['positioningMode'], - ) - ..aspectRatioLock = json['aspectRatioLock'] as bool - ..reactions = (json['reactions'] as List) - .map((e) => Reaction.fromJson(e as Map)) - .toList() ..widthFactor = (json['widthFactor'] as num?)?.toDouble() ..heightFactor = (json['heightFactor'] as num?)?.toDouble() ..interactionStates = @@ -155,16 +164,16 @@ const _$SizeFitEnumMap = { SizeFit.shrinkWrap: 'shrinkWrap', }; -const _$ComponentMarkerTypeEnumMap = { - ComponentMarkerType.component: 'component', - ComponentMarkerType.instance: 'instance', -}; - const _$PositioningModeEnumMap = { PositioningMode.align: 'align', PositioningMode.pin: 'pin', }; +const _$ComponentMarkerTypeEnumMap = { + ComponentMarkerType.component: 'component', + ComponentMarkerType.instance: 'instance', +}; + const _$InteractionStateEnumMap = { InteractionState.hovered: 'hovered', InteractionState.focused: 'focused', diff --git a/test/json_defaults_test.dart b/test/json_defaults_test.dart index 86b91045..1b07631a 100644 --- a/test/json_defaults_test.dart +++ b/test/json_defaults_test.dart @@ -381,4 +381,82 @@ void main() { ); }); }); + + group('JSON from build scripts', () { + test('a spacer needs no positioning, aspect lock or reactions', () { + final node = NodeJsonConverter().fromJson({ + 'type': 'spacer', + 'id': 'sp', + 'name': 'sp', + 'flex': 1, + 'basicBoxLocal': [0, 0, 10, 10], + 'horizontalFit': 'expanded', + 'verticalFit': 'expanded', + })! as SpacerNode; + expect(node.positioningMode, PositioningMode.align); + expect(node.aspectRatioLock, isFalse); + expect(node.reactions, isEmpty); + // Older readers require these keys, so they are still written. + expect( + node.toJson(), + allOf( + containsPair('positioningMode', 'align'), + containsPair('aspectRatioLock', false), + containsPair('reactions', isEmpty), + ), + ); + }); + + test('a grid needs no useFlutterListView or isScrollable', () { + final node = NodeJsonConverter().fromJson({ + 'type': 'gridView', + 'id': 'grid', + 'name': 'grid', + 'basicBoxLocal': [0, 0, 300, 300], + 'allowedTypes': [], + 'deniedTypes': ['gridView'], + 'children': ['item'], + 'properties': { + 'gridDelegate': { + 'type': 'fixedCrossAxisCount', + 'crossAxisCount': 2, + 'shrinkWrapRows': true, + }, + }, + })! as GridViewNode; + expect(node.useFlutterListView, isFalse); + expect(node.isScrollable, isTrue); + expect(node.properties.gridDelegate.shrinkWrapRows, isTrue); + final json = encodeDecode(node.toJson()) as Map; + expect(json, containsPair('useFlutterListView', false)); + expect(json, containsPair('isScrollable', true)); + expect( + (json['properties'] as Map)['gridDelegate'], + containsPair('shrinkWrapRows', true), + ); + expect(NodeJsonConverter().fromJson(json), node); + }); + + test('rows sized to their content leave the item main axis free', () { + final grid = GridViewNode( + id: 'grid', + name: 'grid', + basicBoxLocal: NodeBox(0, 0, 316, 300), + children: ['item'], + properties: GridViewProperties( + gridDelegate: FixedCrossAxisCountGridDelegateProperties( + crossAxisCount: 2, + crossAxisSpacing: 16, + mainAxisExtent: 130, + shrinkWrapRows: true, + ), + ), + ); + final constraints = grid.relegatedConstraintsToChildren(grid)!; + expect(constraints.minWidth, 150); + expect(constraints.maxWidth, 150); + expect(constraints.minHeight, isNull); + expect(constraints.maxHeight, isNull); + }); + }); } From 5983685220f356d255496c5cda8749e817dd4ad6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 17:53:03 +0000 Subject: [PATCH 07/13] RigCheck fields: confirm presentation, toast styling, wrap, scaled frames - ConfirmAction: presentation (dialog | bottomSheet | sideSheet), sheetWidth, maxHeightFactor (0.92) and showDragHandle (true), as on ShowDialogAction. - ShowToastAction: accentColor and accentWidth (4) for a bar down the left edge, iconColor, fontFamily and cornerRadius. - RowColumnNode: wrap, wrapSpacing and wrapRunSpacing, to lay children out like Flutter's Wrap. - FrameNode: contentFit (a Fit, null for none) and contentAlignment (center), to scale content laid out at the design size into the frame. - TextFieldNode.propertyVariables lists errorText and error, so the editor offers them for binding. Every new field is left out of the JSON at its default. Covered by test/rigcheck_fields_test.dart. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011WedhUVWdWR5swqZYMuGfP --- CHANGELOG.md | 10 +++ lib/src/api/models/action/confirm_action.dart | 37 ++++++++ .../api/models/action/confirm_action.g.dart | 26 ++++++ .../api/models/action/show_toast_action.dart | 46 +++++++++- .../models/action/show_toast_action.g.dart | 15 ++++ lib/src/api/nodes/frame_node.dart | 13 +++ lib/src/api/nodes/frame_node.g.dart | 18 ++++ lib/src/api/nodes/row_column_node.dart | 17 ++++ lib/src/api/nodes/row_column_node.g.dart | 11 ++- lib/src/api/nodes/text_field_node.dart | 9 ++ test/rigcheck_fields_test.dart | 90 +++++++++++++++++++ 11 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 test/rigcheck_fields_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 31bc01ae..378abab8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ### New +- `ConfirmAction.presentation`, `sheetWidth`, `maxHeightFactor` and + `showDragHandle`, as on `ShowDialogAction`. +- `ShowToastAction.accentColor`, `accentWidth` (4), `iconColor`, + `fontFamily` and `cornerRadius`. +- `RowColumnNode.wrap`, `wrapSpacing` and `wrapRunSpacing`. +- `FrameNode.contentFit` (a `Fit`, null for none) and `contentAlignment` + (center). +- `TextFieldNode.propertyVariables` lists `errorText` and `error`. +- All new fields are left out of the JSON at their defaults. + - Repeating gradients: `PaintModel.tileMode` (`GradientTileMode` clamp, repeated or mirror; default clamp, left out of the JSON) and `PaintModel.gradientExtent`, the length in pixels of one run of the diff --git a/lib/src/api/models/action/confirm_action.dart b/lib/src/api/models/action/confirm_action.dart index 7d39e4ae..ac709628 100644 --- a/lib/src/api/models/action/confirm_action.dart +++ b/lib/src/api/models/action/confirm_action.dart @@ -18,6 +18,9 @@ part 'confirm_action.g.dart'; /// back" action whose params hold [resultKey]: `true` confirms, anything else /// cancels. Closing the dialog any other way cancels. The params popped with a /// confirm are the `${result}` the [actions] read. +/// +/// Either dialog can be presented as a centered dialog, a bottom sheet or a +/// side sheet ([presentation]), like [ShowDialogAction.presentation]. @JsonSerializable() class ConfirmAction extends ActionModel with Equatable, SerializableMixin { /// The dialog's title. Supports variables and formulas. @@ -71,6 +74,23 @@ class ConfirmAction extends ActionModel with Equatable, SerializableMixin { /// Corner radius of the built-in dialog. Null follows the app's buttons. final double? cornerRadius; + /// How the dialog is presented: centered, as a bottom sheet or as a side + /// sheet. See [ShowDialogAction.presentation]. + @JsonKey(unknownEnumValue: DialogPresentation.dialog) + final DialogPresentation presentation; + + /// Width of a [DialogPresentation.sideSheet], in logical pixels. Null takes + /// the [dialogId] canvas's width, or 400 for the built-in dialog. + final double? sheetWidth; + + /// The most of the screen's height a [DialogPresentation.bottomSheet] takes, + /// from 0 to 1. + final double maxHeightFactor; + + /// Whether a [DialogPresentation.bottomSheet] shows a grab handle at its + /// top. + final bool showDragHandle; + /// The key of the "Navigate back" params that answers a [dialogId] dialog. static const String resultKey = 'confirmed'; @@ -107,6 +127,10 @@ class ConfirmAction extends ActionModel with Equatable, SerializableMixin { this.textColor, this.confirmColor, this.cornerRadius, + this.presentation = DialogPresentation.dialog, + this.sheetWidth, + this.maxHeightFactor = 0.92, + this.showDragHandle = true, super.nonBlocking, super.enabled, }) : actions = actions ?? [], @@ -137,6 +161,11 @@ class ConfirmAction extends ActionModel with Equatable, SerializableMixin { bool clearConfirmColor = false, double? cornerRadius, bool clearCornerRadius = false, + DialogPresentation? presentation, + double? sheetWidth, + bool clearSheetWidth = false, + double? maxHeightFactor, + bool? showDragHandle, }) => ConfirmAction( title: title ?? this.title, message: message ?? this.message, @@ -155,6 +184,10 @@ class ConfirmAction extends ActionModel with Equatable, SerializableMixin { textColor: clearTextColor ? null : textColor ?? this.textColor, confirmColor: clearConfirmColor ? null : confirmColor ?? this.confirmColor, cornerRadius: clearCornerRadius ? null : cornerRadius ?? this.cornerRadius, + presentation: presentation ?? this.presentation, + sheetWidth: clearSheetWidth ? null : sheetWidth ?? this.sheetWidth, + maxHeightFactor: maxHeightFactor ?? this.maxHeightFactor, + showDragHandle: showDragHandle ?? this.showDragHandle, nonBlocking: nonBlocking, enabled: enabled, ); @@ -176,6 +209,10 @@ class ConfirmAction extends ActionModel with Equatable, SerializableMixin { textColor, confirmColor, cornerRadius, + presentation, + sheetWidth, + maxHeightFactor, + showDragHandle, ]; /// Creates an instance from a JSON object. diff --git a/lib/src/api/models/action/confirm_action.g.dart b/lib/src/api/models/action/confirm_action.g.dart index 93096cc1..06c7a25f 100644 --- a/lib/src/api/models/action/confirm_action.g.dart +++ b/lib/src/api/models/action/confirm_action.g.dart @@ -19,6 +19,12 @@ Map _$ConfirmActionOmitDefaults( if (isJsonDefault(instance.params, {})) json.remove('params'); if (isJsonDefault(instance.barrierDismissible, true)) json.remove('barrierDismissible'); + if (isJsonDefault(instance.presentation, DialogPresentation.dialog)) + json.remove('presentation'); + if (isJsonDefault(instance.maxHeightFactor, 0.92)) + json.remove('maxHeightFactor'); + if (isJsonDefault(instance.showDragHandle, true)) + json.remove('showDragHandle'); if (isJsonDefault(instance.nonBlocking, false)) json.remove('nonBlocking'); if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); return json; @@ -56,6 +62,16 @@ ConfirmAction _$ConfirmActionFromJson(Map json) => ConfirmAction( ? null : ColorRGBA.fromJson(json['confirmColor']), cornerRadius: (json['cornerRadius'] as num?)?.toDouble(), + presentation: + $enumDecodeNullable( + _$DialogPresentationEnumMap, + json['presentation'], + unknownValue: DialogPresentation.dialog, + ) ?? + DialogPresentation.dialog, + sheetWidth: (json['sheetWidth'] as num?)?.toDouble(), + maxHeightFactor: (json['maxHeightFactor'] as num?)?.toDouble() ?? 0.92, + showDragHandle: json['showDragHandle'] as bool? ?? true, nonBlocking: json['nonBlocking'] as bool? ?? false, enabled: json['enabled'] as bool? ?? true, )..type = $enumDecode(_$ActionTypeEnumMap, json['type']); @@ -80,8 +96,18 @@ Map _$ConfirmActionToJson(ConfirmAction instance) => 'textColor': ?instance.textColor?.toJson(), 'confirmColor': ?instance.confirmColor?.toJson(), 'cornerRadius': ?instance.cornerRadius, + 'presentation': _$DialogPresentationEnumMap[instance.presentation]!, + 'sheetWidth': ?instance.sheetWidth, + 'maxHeightFactor': instance.maxHeightFactor, + 'showDragHandle': instance.showDragHandle, }; +const _$DialogPresentationEnumMap = { + DialogPresentation.dialog: 'dialog', + DialogPresentation.bottomSheet: 'bottomSheet', + DialogPresentation.sideSheet: 'sideSheet', +}; + const _$ActionTypeEnumMap = { ActionType.navigation: 'navigation', ActionType.showDialog: 'showDialog', diff --git a/lib/src/api/models/action/show_toast_action.dart b/lib/src/api/models/action/show_toast_action.dart index a7deaf5a..3143f763 100644 --- a/lib/src/api/models/action/show_toast_action.dart +++ b/lib/src/api/models/action/show_toast_action.dart @@ -53,7 +53,9 @@ enum ToastPosition { /// [actions]. /// /// Its colors come from [style] and the app's theme unless [backgroundColor] -/// or [textColor] are set. Its font is the app's. +/// or [textColor] are set. Its font is the app's unless [fontFamily] is set. +/// An [accentColor] draws a bar down its left edge, and [iconColor] colors +/// its [icon]. @JsonSerializable() class ShowToastAction extends ActionModel with Equatable, SerializableMixin { /// The message. Supports variables and formulas, e.g. @@ -91,6 +93,24 @@ class ShowToastAction extends ActionModel with Equatable, SerializableMixin { /// [ToastPosition.bottom]. final double bottomOffset; + /// Color of a bar down the toast's left edge, e.g. a yellow or red accent. + /// Null draws no bar. + final ColorRGBA? accentColor; + + /// Width of the [accentColor] bar. + final double accentWidth; + + /// Color of the [icon]. Null takes the message color. + final ColorRGBA? iconColor; + + /// Font family of the message and button, e.g. `Barlow`. Null takes the + /// app's font. + final String? fontFamily; + + /// Corner radius of the toast. Null makes a pill, or follows the app's + /// buttons when the toast has a button. + final double? cornerRadius; + /// Creates a new [ShowToastAction]. ShowToastAction({ required this.message, @@ -103,6 +123,11 @@ class ShowToastAction extends ActionModel with Equatable, SerializableMixin { List? actions, this.icon, this.bottomOffset = 104, + this.accentColor, + this.accentWidth = 4, + this.iconColor, + this.fontFamily, + this.cornerRadius, super.nonBlocking, super.enabled, }) : actions = actions ?? [], @@ -123,6 +148,11 @@ class ShowToastAction extends ActionModel with Equatable, SerializableMixin { actions, icon, bottomOffset, + accentColor, + accentWidth, + iconColor, + fontFamily, + cornerRadius, ]; /// Duplicates this [ShowToastAction] with given data overrides. @@ -140,6 +170,15 @@ class ShowToastAction extends ActionModel with Equatable, SerializableMixin { MultiSourceIconModel? icon, bool clearIcon = false, double? bottomOffset, + ColorRGBA? accentColor, + bool clearAccentColor = false, + double? accentWidth, + ColorRGBA? iconColor, + bool clearIconColor = false, + String? fontFamily, + bool clearFontFamily = false, + double? cornerRadius, + bool clearCornerRadius = false, }) => ShowToastAction( message: message ?? this.message, duration: duration ?? this.duration, @@ -153,6 +192,11 @@ class ShowToastAction extends ActionModel with Equatable, SerializableMixin { actions: actions ?? this.actions, icon: clearIcon ? null : icon ?? this.icon, bottomOffset: bottomOffset ?? this.bottomOffset, + accentColor: clearAccentColor ? null : accentColor ?? this.accentColor, + accentWidth: accentWidth ?? this.accentWidth, + iconColor: clearIconColor ? null : iconColor ?? this.iconColor, + fontFamily: clearFontFamily ? null : fontFamily ?? this.fontFamily, + cornerRadius: clearCornerRadius ? null : cornerRadius ?? this.cornerRadius, nonBlocking: nonBlocking, enabled: enabled, ); diff --git a/lib/src/api/models/action/show_toast_action.g.dart b/lib/src/api/models/action/show_toast_action.g.dart index 684de2c2..8e5996c1 100644 --- a/lib/src/api/models/action/show_toast_action.g.dart +++ b/lib/src/api/models/action/show_toast_action.g.dart @@ -17,6 +17,7 @@ Map _$ShowToastActionOmitDefaults( if (isJsonDefault(instance.actionLabel, '')) json.remove('actionLabel'); if (isJsonDefault(instance.actions, [])) json.remove('actions'); if (isJsonDefault(instance.bottomOffset, 104)) json.remove('bottomOffset'); + if (isJsonDefault(instance.accentWidth, 4)) json.remove('accentWidth'); if (isJsonDefault(instance.nonBlocking, false)) json.remove('nonBlocking'); if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); return json; @@ -57,6 +58,15 @@ ShowToastAction _$ShowToastActionFromJson(Map json) => ShowToastAction( ? null : MultiSourceIconModel.fromJson(json['icon'] as Map), bottomOffset: (json['bottomOffset'] as num?)?.toDouble() ?? 104, + accentColor: json['accentColor'] == null + ? null + : ColorRGBA.fromJson(json['accentColor']), + accentWidth: (json['accentWidth'] as num?)?.toDouble() ?? 4, + iconColor: json['iconColor'] == null + ? null + : ColorRGBA.fromJson(json['iconColor']), + fontFamily: json['fontFamily'] as String?, + cornerRadius: (json['cornerRadius'] as num?)?.toDouble(), nonBlocking: json['nonBlocking'] as bool? ?? false, enabled: json['enabled'] as bool? ?? true, )..type = $enumDecode(_$ActionTypeEnumMap, json['type']); @@ -76,6 +86,11 @@ Map _$ShowToastActionToJson(ShowToastAction instance) => 'actions': instance.actions.map((e) => e.toJson()).toList(), 'icon': ?instance.icon?.toJson(), 'bottomOffset': instance.bottomOffset, + 'accentColor': ?instance.accentColor?.toJson(), + 'accentWidth': instance.accentWidth, + 'iconColor': ?instance.iconColor?.toJson(), + 'fontFamily': ?instance.fontFamily, + 'cornerRadius': ?instance.cornerRadius, }; const _$ToastStyleEnumMap = { diff --git a/lib/src/api/nodes/frame_node.dart b/lib/src/api/nodes/frame_node.dart index aa6419bf..2723aff3 100644 --- a/lib/src/api/nodes/frame_node.dart +++ b/lib/src/api/nodes/frame_node.dart @@ -15,6 +15,17 @@ class FrameNode extends DefaultShapeNode @override final bool supportsPadding = true; + /// Scales the frame's content to fit it, like Flutter's [FittedBox]: the + /// children are laid out in the frame's design-size content box and the + /// result is scaled by this fit into the content box the frame gets, e.g. + /// [Fit.contain] to show a fixed drawing as large as it fits. Null (the + /// default) lays the children out in the frame's actual size, unscaled. + Fit? contentFit; + + /// Where scaled content sits in the frame when [contentFit] leaves space + /// on one axis. + AlignmentModel contentAlignment; + /// Frame is a Rectangle with children, which can be clipped. FrameNode({ required super.id, @@ -73,6 +84,8 @@ class FrameNode extends DefaultShapeNode String? pageID, String? layoutID, bool showPortal = false, + this.contentFit, + this.contentAlignment = AlignmentModel.center, }) { setCornerMixin( cornerRadius: cornerRadius, diff --git a/lib/src/api/nodes/frame_node.g.dart b/lib/src/api/nodes/frame_node.g.dart index deec42ca..7e8f6eb0 100644 --- a/lib/src/api/nodes/frame_node.g.dart +++ b/lib/src/api/nodes/frame_node.g.dart @@ -10,6 +10,8 @@ Map _$FrameNodeOmitDefaults( FrameNode instance, Map json, ) { + if (isJsonDefault(instance.contentAlignment, AlignmentModel.center)) + json.remove('contentAlignment'); if (isJsonDefault(instance.visible, true)) json.remove('visible'); if (excludeConstraintsIf(instance)) json.remove('constraints'); if (isJsonDefault(instance.constraints, const BoxConstraintsModel())) @@ -217,6 +219,10 @@ FrameNode _$FrameNodeFromJson(Map json) => pageID: json['pageID'] as String?, layoutID: json['layoutID'] as String?, showPortal: json['showPortal'] as bool? ?? false, + contentFit: $enumDecodeNullable(_$FitEnumMap, json['contentFit']), + contentAlignment: json['contentAlignment'] == null + ? AlignmentModel.center + : AlignmentModel.fromJson(json['contentAlignment'] as Map), ) ..variables = (json['variables'] as Map?)?.map( @@ -331,6 +337,8 @@ Map _$FrameNodeToJson(FrameNode instance) => { 'layoutID': ?instance.layoutID, 'showPortal': instance.showPortal, 'type': instance.type, + 'contentFit': ?_$FitEnumMap[instance.contentFit], + 'contentAlignment': instance.contentAlignment.toJson(), }; const _$BlendModeCEnumMap = { @@ -416,6 +424,16 @@ const _$ScrollViewKeyboardDismissBehaviorCEnumMap = { ScrollViewKeyboardDismissBehaviorC.onDrag: 'onDrag', }; +const _$FitEnumMap = { + Fit.contain: 'contain', + Fit.cover: 'cover', + Fit.fill: 'fill', + Fit.fitHeight: 'fitHeight', + Fit.fitWidth: 'fitWidth', + Fit.none: 'none', + Fit.scaleDown: 'scaleDown', +}; + const _$ComponentMarkerTypeEnumMap = { ComponentMarkerType.component: 'component', ComponentMarkerType.instance: 'instance', diff --git a/lib/src/api/nodes/row_column_node.dart b/lib/src/api/nodes/row_column_node.dart index 2094d2f7..4e06d7c2 100644 --- a/lib/src/api/nodes/row_column_node.dart +++ b/lib/src/api/nodes/row_column_node.dart @@ -24,6 +24,20 @@ class RowColumnNode extends ParentNode @override set scrollDirection(AxisC value) {} + /// Whether children that don't fit on the main axis wrap onto a new line + /// (a row) or column (a column), like Flutter's [Wrap]. The main axis + /// alignment places each line's children, the cross axis alignment places + /// children within their line (stretch and baseline read as start). + /// Children keep their own sizes: a child that flexes on the main axis + /// fills a whole line, and spacers take no space. + bool wrap; + + /// The gap between children on a line when [wrap] is on. + double wrapSpacing; + + /// The gap between lines when [wrap] is on. + double wrapRunSpacing; + /// Creates a [RowColumnNode] with the given data. RowColumnNode({ required super.id, @@ -76,6 +90,9 @@ class RowColumnNode extends ParentNode ScrollViewKeyboardDismissBehaviorC.manual, bool useFlutterListView = false, bool shouldAlwaysScroll = true, + this.wrap = false, + this.wrapSpacing = 0, + this.wrapRunSpacing = 0, }) { setCornerMixin( cornerRadius: cornerRadius, diff --git a/lib/src/api/nodes/row_column_node.g.dart b/lib/src/api/nodes/row_column_node.g.dart index 01243e44..0f2f5f5d 100644 --- a/lib/src/api/nodes/row_column_node.g.dart +++ b/lib/src/api/nodes/row_column_node.g.dart @@ -10,6 +10,9 @@ Map _$RowColumnNodeOmitDefaults( RowColumnNode instance, Map json, ) { + if (isJsonDefault(instance.wrap, false)) json.remove('wrap'); + if (isJsonDefault(instance.wrapSpacing, 0)) json.remove('wrapSpacing'); + if (isJsonDefault(instance.wrapRunSpacing, 0)) json.remove('wrapRunSpacing'); if (isJsonDefault(instance.visible, true)) json.remove('visible'); if (excludeConstraintsIf(instance)) json.remove('constraints'); if (isJsonDefault(instance.constraints, const BoxConstraintsModel())) @@ -227,6 +230,9 @@ RowColumnNode _$RowColumnNodeFromJson(Map json) => ScrollViewKeyboardDismissBehaviorC.manual, useFlutterListView: json['useFlutterListView'] as bool? ?? false, shouldAlwaysScroll: json['shouldAlwaysScroll'] as bool? ?? true, + wrap: json['wrap'] as bool? ?? false, + wrapSpacing: (json['wrapSpacing'] as num?)?.toDouble() ?? 0, + wrapRunSpacing: (json['wrapRunSpacing'] as num?)?.toDouble() ?? 0, ) ..variables = (json['variables'] as Map?)?.map( @@ -280,6 +286,7 @@ RowColumnNode _$RowColumnNodeFromJson(Map json) => Map _$RowColumnNodeToJson( RowColumnNode instance, ) => { + 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -297,7 +304,6 @@ Map _$RowColumnNodeToJson( 'flex': instance.flex, 'aspectRatioLock': instance.aspectRatioLock, 'alignment': instance.alignment.toJson(), - 'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'basicBoxLocal': instance.basicBoxLocal.toJson(), 'margin': instance.margin.toJson(), 'padding': instance.padding.toJson(), @@ -342,6 +348,9 @@ Map _$RowColumnNodeToJson( 'useFlutterListView': instance.useFlutterListView, 'shouldAlwaysScroll': instance.shouldAlwaysScroll, 'type': instance.type, + 'wrap': instance.wrap, + 'wrapSpacing': instance.wrapSpacing, + 'wrapRunSpacing': instance.wrapRunSpacing, }; const _$RowColumnTypeEnumMap = { diff --git a/lib/src/api/nodes/text_field_node.dart b/lib/src/api/nodes/text_field_node.dart index db991e14..a0baede7 100644 --- a/lib/src/api/nodes/text_field_node.dart +++ b/lib/src/api/nodes/text_field_node.dart @@ -116,6 +116,15 @@ class TextFieldNode extends SceneNode value: properties.decoration.labelText ?? '', ), StringValue(name: 'hintText', value: properties.decoration.hintText ?? ''), + // The error message under the field. Bind it to a formula such as + // `${= showErrors && length(note) < 10 ? "Too short" : ""}`: an empty + // result means no error. + StringValue( + name: 'errorText', + value: properties.decoration.errorText ?? '', + ), + // Shows the error borders without a message when true. + BoolValue(name: 'error', value: false), ]; @override diff --git a/test/rigcheck_fields_test.dart b/test/rigcheck_fields_test.dart new file mode 100644 index 00000000..f62950a5 --- /dev/null +++ b/test/rigcheck_fields_test.dart @@ -0,0 +1,90 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('ConfirmAction reads presentation like ShowDialogAction', () { + final ConfirmAction action = ConfirmAction.fromJson({ + 'type': 'confirm', + 'title': 'Reopen DF-3104?', + 'dialogId': 'reopen', + 'presentation': 'sideSheet', + 'sheetWidth': 440, + 'showDragHandle': false, + }); + expect(action.presentation, DialogPresentation.sideSheet); + expect(action.sheetWidth, 440); + expect(action.showDragHandle, isFalse); + expect(ConfirmAction.fromJson(action.toJson()), action); + expect( + ConfirmAction.fromJson({ + 'type': 'confirm', + 'title': 'x', + 'presentation': 'popover', + }).presentation, + DialogPresentation.dialog, + ); + expect( + ConfirmAction(title: 'x').toJson().keys, + isNot(contains('sheetWidth')), + ); + }); + + test('ShowToastAction keeps its accent, icon color, font and radius', () { + final ShowToastAction toast = ShowToastAction( + message: 'Saved', + accentColor: ColorRGBA.black, + iconColor: ColorRGBA.white, + fontFamily: 'Barlow', + cornerRadius: 4, + ); + final ShowToastAction copy = ShowToastAction.fromJson(toast.toJson()); + expect(copy, toast); + expect(copy.accentWidth, 4); + expect( + copy.copyWith(clearAccentColor: true, clearFontFamily: true), + isA() + .having((t) => t.accentColor, 'accentColor', isNull) + .having((t) => t.fontFamily, 'fontFamily', isNull), + ); + }); + + test('a text field can bind errorText and error', () { + final TextFieldNode field = TextFieldNode( + id: 'f', + name: 'f', + basicBoxLocal: NodeBox(0, 0, 100, 40), + properties: TextFieldProperties(), + ); + expect( + field.propertyVariables.map((value) => value.name), + containsAll(['errorText', 'error']), + ); + }); + + test('rowColumn wrap and frame contentFit read from JSON', () { + final RowColumnNode row = RowColumnNode.fromJson({ + 'id': 'r', + 'name': 'r', + 'type': 'rowColumn', + 'rowColumnType': 'row', + 'basicBoxLocal': [0, 0, 100, 40], + 'children': [], + 'wrap': true, + 'wrapSpacing': 24, + 'wrapRunSpacing': 8, + }); + expect(row.wrap, isTrue); + expect(row.wrapSpacing, 24); + expect(row.wrapRunSpacing, 8); + final FrameNode frame = FrameNode.fromJson({ + 'id': 'f', + 'name': 'f', + 'type': 'frame', + 'basicBoxLocal': [0, 0, 300, 660], + 'children': [], + 'contentFit': 'contain', + }); + expect(frame.contentFit, Fit.contain); + expect(frame.contentAlignment, AlignmentModel.center); + }); +} From 6dd9780756a9a92d43652097d1163d8aa9919df0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 17:57:40 +0000 Subject: [PATCH 08/13] Test what stored node JSON costs and that it holds no defaults Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011WedhUVWdWR5swqZYMuGfP --- test/node_json_size_test.dart | 101 ++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 test/node_json_size_test.dart diff --git a/test/node_json_size_test.dart b/test/node_json_size_test.dart new file mode 100644 index 00000000..0fa7e625 --- /dev/null +++ b/test/node_json_size_test.dart @@ -0,0 +1,101 @@ +import 'dart:convert'; + +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +/// What a page document pays per node, and that none of it is a default: +/// a key whose removal reads back as the same node is a value `toJson` +/// should have left out (see `tool/json_defaults_builder`). Typical nodes of +/// a built screen, as the editor stores them. +void main() { + test('stored node JSON is small and carries no default values', () { + final box = NodeBox(16, 120, 358, 48); + final nodes = [ + RowColumnNode( + id: 'ZPcdmSV2nPmmNYlEbP5a', + name: 'Rig card', + basicBoxLocal: box, + children: ['a1b2c3d4e5f6g7h8i9j0'], + rowColumnType: RowColumnType.row, + mainAxisAlignment: MainAxisAlignmentC.start, + padding: const EdgeInsetsModel.all(16), + horizontalFit: SizeFit.expanded, + verticalFit: SizeFit.shrinkWrap, + fills: [PaintModel.solid(color: ColorRGB(r: 1, g: 1, b: 1), id: 'p1')], + strokes: [ + PaintModel.solid(color: ColorRGB(r: 0.9, g: 0.9, b: 0.9), id: 'p4'), + ], + strokeWeight: 1, + cornerRadius: CornerRadius.all(RadiusModel.circular(12)), + reactions: [ + Reaction( + trigger: TriggerModel(type: TriggerType.click), + action: NavigationAction(destinationId: 'abcdefghij0123456789'), + ), + ], + ), + TextNode( + id: 'a1b2c3d4e5f6g7h8i9j0', + name: 'Title', + basicBoxLocal: box, + characters: r'Unit ${vehicle.unit}', + horizontalFit: SizeFit.shrinkWrap, + verticalFit: SizeFit.shrinkWrap, + textMixedProps: [ + StartEndProp( + start: 0, + end: 20, + fontSize: 16, + lineHeight: LineHeight.auto, + letterSpacing: LetterSpacing.zero, + fontName: FontName( + family: 'Inter', + style: 'Bold', + weight: FontWeightNumeric.w700, + ), + fills: [ + PaintModel.solid( + color: ColorRGB(r: 0.1, g: 0.1, b: 0.1), + id: 'p2', + ), + ], + ), + ], + ), + RectangleNode( + id: 'b1b2c3d4e5f6g7h8i9j0', + name: 'Divider', + basicBoxLocal: box, + fills: [ + PaintModel.solid(color: ColorRGB(r: 0.9, g: 0.9, b: 0.9), id: 'p3'), + ], + ), + ]; + const maxBytes = {'rowColumn': 600, 'text': 380, 'rectangle': 160}; + for (final node in nodes) { + final json = + jsonDecode(jsonEncode(node.toJson())) as Map; + final int bytes = utf8.encode(jsonEncode(json)).length; + print('${node.type}: $bytes bytes ${jsonEncode(json)}'); + expect(bytes, lessThanOrEqualTo(maxBytes[node.type]!), reason: node.type); + // Reads back the same. + expect( + jsonEncode(NodeJsonConverter.instance.fromJson(json)!.toJson()), + jsonEncode(json), + ); + for (final key in json.keys) { + if (key == 'type' || key == 'id') continue; + final without = Map.from(json)..remove(key); + Object? back; + try { + back = jsonEncode( + NodeJsonConverter.instance.fromJson(without)!.toJson(), + ); + } catch (_) { + continue; // A required key. + } + expect(back, isNot(jsonEncode(json)), reason: '${node.type}.$key'); + } + } + }); +} From c61552ab64d7aaca0146b43a5b0c947858adfcd9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:27:23 +0000 Subject: [PATCH 09/13] Changelog: the RigCheck additions under Unreleased Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011WedhUVWdWR5swqZYMuGfP --- CHANGELOG.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 378abab8..2d29be8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ ### New +- `SignatureNode` (`signature`), a draw-to-sign pad. Its value is the list + of strokes drawn (each a list of `[x, y]` points), bindable through + `variables.value` and settable with a set value action; `changed` fires + when a stroke ends. Properties: ink color and width, background, border + and error border, corner radius, placeholder, sign line, and bindable + `error` and `enabled`. +- `BaseNode.loopAnimation` (`LoopAnimation`: pulse, fade, scale or spin, + with duration, delay, reverse, curve and from/to scale, opacity or + turns), on every node type and left out of the JSON when null. + `loopAnimationEnabled` can be bound to turn it on and off. +- Nodes with a click trigger also offer `cursorEnter` and `cursorExit`; + the trigger docs say what `hover`, `cursorEnter` and `cursorExit` mean. +- `ShowDialogAction.presentation` (dialog, bottomSheet or sideSheet), + `sheetWidth`, `maxHeightFactor` (0.92) and `showDragHandle` (true). + `copyWith` keeps `nonBlocking` and `enabled`. - `ConfirmAction.presentation`, `sheetWidth`, `maxHeightFactor` and `showDragHandle`, as on `ShowDialogAction`. - `ShowToastAction.accentColor`, `accentWidth` (4), `iconColor`, @@ -11,7 +26,6 @@ (center). - `TextFieldNode.propertyVariables` lists `errorText` and `error`. - All new fields are left out of the JSON at their defaults. - - Repeating gradients: `PaintModel.tileMode` (`GradientTileMode` clamp, repeated or mirror; default clamp, left out of the JSON) and `PaintModel.gradientExtent`, the length in pixels of one run of the From 1be8690f0bfbaea7cab093dcdd9206cb8894037b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:31:37 +0000 Subject: [PATCH 10/13] Lint: const constructors in the text field node and the JSON size test Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011WedhUVWdWR5swqZYMuGfP --- lib/src/api/nodes/text_field_node.dart | 2 +- test/node_json_size_test.dart | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/src/api/nodes/text_field_node.dart b/lib/src/api/nodes/text_field_node.dart index a0baede7..fd569c98 100644 --- a/lib/src/api/nodes/text_field_node.dart +++ b/lib/src/api/nodes/text_field_node.dart @@ -124,7 +124,7 @@ class TextFieldNode extends SceneNode value: properties.decoration.errorText ?? '', ), // Shows the error borders without a message when true. - BoolValue(name: 'error', value: false), + const BoolValue(name: 'error', value: false), ]; @override diff --git a/test/node_json_size_test.dart b/test/node_json_size_test.dart index 0fa7e625..3dd2e525 100644 --- a/test/node_json_size_test.dart +++ b/test/node_json_size_test.dart @@ -21,12 +21,17 @@ void main() { padding: const EdgeInsetsModel.all(16), horizontalFit: SizeFit.expanded, verticalFit: SizeFit.shrinkWrap, - fills: [PaintModel.solid(color: ColorRGB(r: 1, g: 1, b: 1), id: 'p1')], + fills: [ + PaintModel.solid(color: const ColorRGB(r: 1, g: 1, b: 1), id: 'p1'), + ], strokes: [ - PaintModel.solid(color: ColorRGB(r: 0.9, g: 0.9, b: 0.9), id: 'p4'), + PaintModel.solid( + color: const ColorRGB(r: 0.9, g: 0.9, b: 0.9), + id: 'p4', + ), ], strokeWeight: 1, - cornerRadius: CornerRadius.all(RadiusModel.circular(12)), + cornerRadius: const CornerRadius.all(const RadiusModel.circular(12)), reactions: [ Reaction( trigger: TriggerModel(type: TriggerType.click), @@ -48,14 +53,14 @@ void main() { fontSize: 16, lineHeight: LineHeight.auto, letterSpacing: LetterSpacing.zero, - fontName: FontName( + fontName: const FontName( family: 'Inter', style: 'Bold', weight: FontWeightNumeric.w700, ), fills: [ PaintModel.solid( - color: ColorRGB(r: 0.1, g: 0.1, b: 0.1), + color: const ColorRGB(r: 0.1, g: 0.1, b: 0.1), id: 'p2', ), ], @@ -67,7 +72,10 @@ void main() { name: 'Divider', basicBoxLocal: box, fills: [ - PaintModel.solid(color: ColorRGB(r: 0.9, g: 0.9, b: 0.9), id: 'p3'), + PaintModel.solid( + color: const ColorRGB(r: 0.9, g: 0.9, b: 0.9), + id: 'p3', + ), ], ), ]; From c4ea8a0e568e45ee83e22d21378a16091d76c739 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:32:06 +0000 Subject: [PATCH 11/13] Lint: drop a redundant const in the JSON size test Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011WedhUVWdWR5swqZYMuGfP --- test/node_json_size_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/node_json_size_test.dart b/test/node_json_size_test.dart index 3dd2e525..bd50ff3b 100644 --- a/test/node_json_size_test.dart +++ b/test/node_json_size_test.dart @@ -31,7 +31,7 @@ void main() { ), ], strokeWeight: 1, - cornerRadius: const CornerRadius.all(const RadiusModel.circular(12)), + cornerRadius: const CornerRadius.all(RadiusModel.circular(12)), reactions: [ Reaction( trigger: TriggerModel(type: TriggerType.click), From d9c0eead79952cdee9bf0a61e7f5f3024606b757 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:38:56 +0000 Subject: [PATCH 12/13] Format the JSON defaults test Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011WedhUVWdWR5swqZYMuGfP --- test/json_defaults_test.dart | 54 +++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/test/json_defaults_test.dart b/test/json_defaults_test.dart index 1b07631a..44647661 100644 --- a/test/json_defaults_test.dart +++ b/test/json_defaults_test.dart @@ -384,15 +384,17 @@ void main() { group('JSON from build scripts', () { test('a spacer needs no positioning, aspect lock or reactions', () { - final node = NodeJsonConverter().fromJson({ - 'type': 'spacer', - 'id': 'sp', - 'name': 'sp', - 'flex': 1, - 'basicBoxLocal': [0, 0, 10, 10], - 'horizontalFit': 'expanded', - 'verticalFit': 'expanded', - })! as SpacerNode; + final node = + NodeJsonConverter().fromJson({ + 'type': 'spacer', + 'id': 'sp', + 'name': 'sp', + 'flex': 1, + 'basicBoxLocal': [0, 0, 10, 10], + 'horizontalFit': 'expanded', + 'verticalFit': 'expanded', + })! + as SpacerNode; expect(node.positioningMode, PositioningMode.align); expect(node.aspectRatioLock, isFalse); expect(node.reactions, isEmpty); @@ -408,22 +410,24 @@ void main() { }); test('a grid needs no useFlutterListView or isScrollable', () { - final node = NodeJsonConverter().fromJson({ - 'type': 'gridView', - 'id': 'grid', - 'name': 'grid', - 'basicBoxLocal': [0, 0, 300, 300], - 'allowedTypes': [], - 'deniedTypes': ['gridView'], - 'children': ['item'], - 'properties': { - 'gridDelegate': { - 'type': 'fixedCrossAxisCount', - 'crossAxisCount': 2, - 'shrinkWrapRows': true, - }, - }, - })! as GridViewNode; + final node = + NodeJsonConverter().fromJson({ + 'type': 'gridView', + 'id': 'grid', + 'name': 'grid', + 'basicBoxLocal': [0, 0, 300, 300], + 'allowedTypes': [], + 'deniedTypes': ['gridView'], + 'children': ['item'], + 'properties': { + 'gridDelegate': { + 'type': 'fixedCrossAxisCount', + 'crossAxisCount': 2, + 'shrinkWrapRows': true, + }, + }, + })! + as GridViewNode; expect(node.useFlutterListView, isFalse); expect(node.isScrollable, isTrue); expect(node.properties.gridDelegate.shrinkWrapRows, isTrue); From 5d5c5bf68a939769fdff8df47f98fe18114e96a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 00:24:22 +0000 Subject: [PATCH 13/13] Sheets keep master's look by default: no drag handle, no height cap Saved bottom sheets (released in 6.2.0) don't change when this branch adds the options: showDragHandle defaults to false and maxHeightFactor to 1. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011WedhUVWdWR5swqZYMuGfP --- CHANGELOG.md | 3 ++- lib/src/api/models/action/confirm_action.dart | 4 ++-- lib/src/api/models/action/confirm_action.g.dart | 8 ++++---- lib/src/api/models/action/show_dialog_action.dart | 4 ++-- lib/src/api/models/action/show_dialog_action.g.dart | 8 ++++---- test/rigcheck_fields_test.dart | 4 ++-- test/show_dialog_action_test.dart | 8 ++++---- 7 files changed, 20 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc1e45da..dc60a8a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ - Nodes with a click trigger also offer `cursorEnter` and `cursorExit`; the trigger docs say what `hover`, `cursorEnter` and `cursorExit` mean. - `DialogPresentation.sideSheet`, and `ShowDialogAction.sheetWidth`, - `maxHeightFactor` (0.92) and `showDragHandle` (true) for sheets. + `maxHeightFactor` (1: no cap) and `showDragHandle` (false) for sheets, + so saved bottom sheets look as before. `copyWith` keeps `nonBlocking` and `enabled`. - `ConfirmAction.presentation`, `sheetWidth`, `maxHeightFactor` and `showDragHandle`, as on `ShowDialogAction`. diff --git a/lib/src/api/models/action/confirm_action.dart b/lib/src/api/models/action/confirm_action.dart index ac709628..8734f438 100644 --- a/lib/src/api/models/action/confirm_action.dart +++ b/lib/src/api/models/action/confirm_action.dart @@ -129,8 +129,8 @@ class ConfirmAction extends ActionModel with Equatable, SerializableMixin { this.cornerRadius, this.presentation = DialogPresentation.dialog, this.sheetWidth, - this.maxHeightFactor = 0.92, - this.showDragHandle = true, + this.maxHeightFactor = 1, + this.showDragHandle = false, super.nonBlocking, super.enabled, }) : actions = actions ?? [], diff --git a/lib/src/api/models/action/confirm_action.g.dart b/lib/src/api/models/action/confirm_action.g.dart index 9bf5b01e..5e1888e0 100644 --- a/lib/src/api/models/action/confirm_action.g.dart +++ b/lib/src/api/models/action/confirm_action.g.dart @@ -21,9 +21,9 @@ Map _$ConfirmActionOmitDefaults( json.remove('barrierDismissible'); if (isJsonDefault(instance.presentation, DialogPresentation.dialog)) json.remove('presentation'); - if (isJsonDefault(instance.maxHeightFactor, 0.92)) + if (isJsonDefault(instance.maxHeightFactor, 1)) json.remove('maxHeightFactor'); - if (isJsonDefault(instance.showDragHandle, true)) + if (isJsonDefault(instance.showDragHandle, false)) json.remove('showDragHandle'); if (isJsonDefault(instance.nonBlocking, false)) json.remove('nonBlocking'); if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); @@ -70,8 +70,8 @@ ConfirmAction _$ConfirmActionFromJson(Map json) => ConfirmAction( ) ?? DialogPresentation.dialog, sheetWidth: (json['sheetWidth'] as num?)?.toDouble(), - maxHeightFactor: (json['maxHeightFactor'] as num?)?.toDouble() ?? 0.92, - showDragHandle: json['showDragHandle'] as bool? ?? true, + maxHeightFactor: (json['maxHeightFactor'] as num?)?.toDouble() ?? 1, + showDragHandle: json['showDragHandle'] as bool? ?? false, nonBlocking: json['nonBlocking'] as bool? ?? false, enabled: json['enabled'] as bool? ?? true, )..type = $enumDecode(_$ActionTypeEnumMap, json['type']); diff --git a/lib/src/api/models/action/show_dialog_action.dart b/lib/src/api/models/action/show_dialog_action.dart index 76fbb060..c73beab0 100644 --- a/lib/src/api/models/action/show_dialog_action.dart +++ b/lib/src/api/models/action/show_dialog_action.dart @@ -56,8 +56,8 @@ class ShowDialogAction extends ActionModel with Equatable, SerializableMixin { this.presentation = DialogPresentation.dialog, this.alignment = AlignmentModel.center, this.sheetWidth, - this.maxHeightFactor = 0.92, - this.showDragHandle = true, + this.maxHeightFactor = 1, + this.showDragHandle = false, super.nonBlocking, super.enabled, }) : params = {...params ?? {}}, diff --git a/lib/src/api/models/action/show_dialog_action.g.dart b/lib/src/api/models/action/show_dialog_action.g.dart index dc2c116a..365505f5 100644 --- a/lib/src/api/models/action/show_dialog_action.g.dart +++ b/lib/src/api/models/action/show_dialog_action.g.dart @@ -20,9 +20,9 @@ Map _$ShowDialogActionOmitDefaults( json.remove('presentation'); if (isJsonDefault(instance.alignment, AlignmentModel.center)) json.remove('alignment'); - if (isJsonDefault(instance.maxHeightFactor, 0.92)) + if (isJsonDefault(instance.maxHeightFactor, 1)) json.remove('maxHeightFactor'); - if (isJsonDefault(instance.showDragHandle, true)) + if (isJsonDefault(instance.showDragHandle, false)) json.remove('showDragHandle'); if (isJsonDefault(instance.nonBlocking, false)) json.remove('nonBlocking'); if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); @@ -54,8 +54,8 @@ ShowDialogAction _$ShowDialogActionFromJson(Map json) => ShowDialogAction( ? AlignmentModel.center : AlignmentModel.fromJson(json['alignment'] as Map), sheetWidth: (json['sheetWidth'] as num?)?.toDouble(), - maxHeightFactor: (json['maxHeightFactor'] as num?)?.toDouble() ?? 0.92, - showDragHandle: json['showDragHandle'] as bool? ?? true, + maxHeightFactor: (json['maxHeightFactor'] as num?)?.toDouble() ?? 1, + showDragHandle: json['showDragHandle'] as bool? ?? false, nonBlocking: json['nonBlocking'] as bool? ?? false, enabled: json['enabled'] as bool? ?? true, )..type = $enumDecode(_$ActionTypeEnumMap, json['type']); diff --git a/test/rigcheck_fields_test.dart b/test/rigcheck_fields_test.dart index dfc10beb..f0f75afd 100644 --- a/test/rigcheck_fields_test.dart +++ b/test/rigcheck_fields_test.dart @@ -9,11 +9,11 @@ void main() { 'dialogId': 'reopen', 'presentation': 'sideSheet', 'sheetWidth': 440, - 'showDragHandle': false, + 'showDragHandle': true, }); expect(action.presentation, DialogPresentation.sideSheet); expect(action.sheetWidth, 440); - expect(action.showDragHandle, isFalse); + expect(action.showDragHandle, isTrue); expect(ConfirmAction.fromJson(action.toJson()), action); expect( ConfirmAction.fromJson({ diff --git a/test/show_dialog_action_test.dart b/test/show_dialog_action_test.dart index 048cc0f7..d54d1ad1 100644 --- a/test/show_dialog_action_test.dart +++ b/test/show_dialog_action_test.dart @@ -10,8 +10,8 @@ void main() { }) as ShowDialogAction; expect(action.presentation, DialogPresentation.dialog); expect(action.sheetWidth, isNull); - expect(action.maxHeightFactor, 0.92); - expect(action.showDragHandle, isTrue); + expect(action.maxHeightFactor, 1); + expect(action.showDragHandle, isFalse); expect(action.toJson(), { 'type': 'showDialog', 'destinationId': 'canvas-1', @@ -26,12 +26,12 @@ void main() { 'params': {}, 'presentation': 'bottomSheet', 'maxHeightFactor': 0.8, - 'showDragHandle': false, + 'showDragHandle': true, }; final action = ActionModel.fromJson(json) as ShowDialogAction; expect(action.presentation, DialogPresentation.bottomSheet); expect(action.maxHeightFactor, 0.8); - expect(action.showDragHandle, isFalse); + expect(action.showDragHandle, isTrue); expect(action.toJson(), json); expect(ActionModel.fromJson(action.toJson()), action); });