From 44df65d4e0aa4895e60091376d5ca47b2dc4feeb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 22:54:00 +0000 Subject: [PATCH 1/9] Motion, timer/key/swipe triggers, stacked lists, list JSON defaults - MotionModel on every node (BaseNode.motion): offset, scale, opacity, fractional rotation, 3D tilt with perspective, origin, an animated transition and a continuous spin. Values bind to formulas through variables['motion.']. - Triggers: timer (intervalMs) and keyPress (key) on canvases, and swipeLeft/Right/Up/Down on frames and rectangles. - ListViewProperties.stacked draws data list items on top of each other. - Lists parse from JSON without allowedTypes, deniedTypes, isScrollable or useFlutterListView. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013oMokUGNW6txknJs7xxTik --- lib/src/api/models/models.dart | 1 + lib/src/api/models/motion.dart | 232 ++++++++++++++++++ lib/src/api/models/motion.g.dart | 79 ++++++ lib/src/api/models/trigger.dart | 67 ++++- lib/src/api/models/trigger.g.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.g.dart | 4 + lib/src/api/nodes/base_node.dart | 4 + lib/src/api/nodes/button_node.g.dart | 4 + lib/src/api/nodes/canvas_node.dart | 2 + lib/src/api/nodes/canvas_node.g.dart | 4 + lib/src/api/nodes/checkbox_node.g.dart | 4 + lib/src/api/nodes/chip_node.g.dart | 4 + lib/src/api/nodes/divider_node.g.dart | 4 + 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.g.dart | 4 + .../nodes/floating_action_button_node.g.dart | 4 + lib/src/api/nodes/frame_node.dart | 1 + 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 | 26 +- lib/src/api/nodes/icon_node.g.dart | 4 + lib/src/api/nodes/list_tile_node.g.dart | 4 + lib/src/api/nodes/list_view_node.dart | 28 ++- lib/src/api/nodes/list_view_node.g.dart | 29 ++- .../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 | 24 +- lib/src/api/nodes/placeholder_node.g.dart | 4 + lib/src/api/nodes/progressbar_node.g.dart | 4 + lib/src/api/nodes/radio_node.g.dart | 6 +- lib/src/api/nodes/rectangle_node.dart | 1 + 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 + .../api/nodes/single_placeholder_node.dart | 2 + .../api/nodes/single_placeholder_node.g.dart | 24 +- 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.g.dart | 4 + lib/src/api/nodes/variance_node.g.dart | 24 +- lib/src/api/nodes/web_view_node.g.dart | 4 + test/placeholder_json_defaults_test.dart | 21 ++ 50 files changed, 680 insertions(+), 45 deletions(-) create mode 100644 lib/src/api/models/motion.dart create mode 100644 lib/src/api/models/motion.g.dart create mode 100644 test/placeholder_json_defaults_test.dart diff --git a/lib/src/api/models/models.dart b/lib/src/api/models/models.dart index 3b86dfac..5c95484f 100644 --- a/lib/src/api/models/models.dart +++ b/lib/src/api/models/models.dart @@ -51,6 +51,7 @@ export 'input_border.dart'; export 'input_decoration.dart'; export 'letter_spacing.dart'; export 'line_height.dart'; +export 'motion.dart'; export 'list_tile_control_affinity.dart'; export 'navigation_bar_item.dart'; export 'node_box.dart'; diff --git a/lib/src/api/models/motion.dart b/lib/src/api/models/motion.dart new file mode 100644 index 00000000..69aa4704 --- /dev/null +++ b/lib/src/api/models/motion.dart @@ -0,0 +1,232 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../../../codelessly_api.dart'; + +part 'motion.g.dart'; + +/// The easing a [MotionModel] transition follows. +enum MotionCurve { + /// Constant speed. + linear, + + /// Starts slow, ends slow. + easeInOut, + + /// Starts fast, ends slow. + easeOut, + + /// Starts slow, ends fast. + easeIn, + + /// Overshoots a little and settles, like a spring. + easeOutBack, + + /// A fast cubic deceleration. + easeOutCubic; + + /// Displayable name. + String get prettify => switch (this) { + MotionCurve.linear => 'Linear', + MotionCurve.easeInOut => 'Ease in out', + MotionCurve.easeOut => 'Ease out', + MotionCurve.easeIn => 'Ease in', + MotionCurve.easeOutBack => 'Overshoot', + MotionCurve.easeOutCubic => 'Decelerate', + }; +} + +/// How a node moves: a transform drawn on top of its layout (it never changes +/// the space the node takes up), an animated transition whenever a value +/// changes, and an optional continuous spin. +/// +/// Every value can be bound to a variable or formula through +/// [BaseNode.variables] using the property names in [MotionModel.properties] +/// (for example `variables['motion.rotateX'] = '${= front - index}'`), so a +/// motion can follow app state: a sleeve tips forward when it is flipped, a +/// tonearm swings in as a side plays, a platter spins while it is playing. +@JsonSerializable() +class MotionModel with Equatable, SerializableMixin { + /// Horizontal offset in logical pixels. + final double translateX; + + /// Vertical offset in logical pixels. + final double translateY; + + /// Uniform scale; 1 is the node's own size. + final double scale; + + /// Opacity from 0 (invisible) to 1, animated like the other values, on + /// top of the node's own opacity. + final double opacity; + + /// Rotation in the plane, in degrees. Unlike [BaseNode.rotationDegrees] + /// this doesn't change the node's layout box, accepts fractions and can be + /// animated. + final double rotate; + + /// Tilt around the horizontal axis, in degrees. Positive tips the top + /// toward the viewer. + final double rotateX; + + /// Tilt around the vertical axis, in degrees. Positive turns the right + /// edge toward the viewer. + final double rotateY; + + /// Depth of the 3D tilt: the larger, the stronger the perspective. + /// 0 draws tilts flat (orthographic). + final double perspective; + + /// The point everything scales, rotates and tilts around, in the node's + /// box: (-1, -1) is the top left, (0, 0) the centre, (1, 1) the bottom + /// right. + final double originX; + + /// See [originX]. + final double originY; + + /// Duration of the animation when any value changes, in milliseconds. + /// 0 jumps straight to the new values. + final int durationMs; + + /// Easing of the transition. + final MotionCurve curve; + + /// Continuous rotation in turns per second (0.555 is 33⅓ rpm). 0 stops. + /// Changes in speed ease in and out over [spinRampMs], like a motor + /// coming up to speed. + final double spin; + + /// How long the spin takes to reach a new speed, in milliseconds. + final int spinRampMs; + + /// Creates a [MotionModel]. + const MotionModel({ + this.translateX = 0, + this.translateY = 0, + this.scale = 1, + this.opacity = 1, + this.rotate = 0, + this.rotateX = 0, + this.rotateY = 0, + this.perspective = 1, + this.originX = 0, + this.originY = 0, + this.durationMs = 0, + this.curve = MotionCurve.easeInOut, + this.spin = 0, + this.spinRampMs = 800, + }); + + /// No motion at all. + static const MotionModel none = MotionModel(); + + /// The names of the values that can be bound to variables, as + /// `motion.` keys of [BaseNode.variables]. + static const List properties = [ + 'translateX', + 'translateY', + 'scale', + 'opacity', + 'rotate', + 'rotateX', + 'rotateY', + 'spin', + ]; + + /// The key in [BaseNode.variables] that binds [property]. + static String variableKey(String property) => 'motion.$property'; + + /// Whether this changes how the node is drawn when nothing is bound. + bool get hasTransform => + translateX != 0 || + translateY != 0 || + scale != 1 || + opacity != 1 || + rotate != 0 || + rotateX != 0 || + rotateY != 0; + + /// The value of [property] in this model. + double valueOf(String property) => switch (property) { + 'translateX' => translateX, + 'translateY' => translateY, + 'scale' => scale, + 'opacity' => opacity, + 'rotate' => rotate, + 'rotateX' => rotateX, + 'rotateY' => rotateY, + 'spin' => spin, + _ => throw ArgumentError.value(property, 'property'), + }; + + /// Returns a copy with the given values replaced. + MotionModel copyWith({ + double? translateX, + double? translateY, + double? scale, + double? opacity, + double? rotate, + double? rotateX, + double? rotateY, + double? perspective, + double? originX, + double? originY, + int? durationMs, + MotionCurve? curve, + double? spin, + int? spinRampMs, + }) => MotionModel( + translateX: translateX ?? this.translateX, + translateY: translateY ?? this.translateY, + scale: scale ?? this.scale, + opacity: opacity ?? this.opacity, + rotate: rotate ?? this.rotate, + rotateX: rotateX ?? this.rotateX, + rotateY: rotateY ?? this.rotateY, + perspective: perspective ?? this.perspective, + originX: originX ?? this.originX, + originY: originY ?? this.originY, + durationMs: durationMs ?? this.durationMs, + curve: curve ?? this.curve, + spin: spin ?? this.spin, + spinRampMs: spinRampMs ?? this.spinRampMs, + ); + + /// Returns a copy with [property] set to [value]. + MotionModel withValue(String property, double value) => switch (property) { + 'translateX' => copyWith(translateX: value), + 'translateY' => copyWith(translateY: value), + 'scale' => copyWith(scale: value), + 'opacity' => copyWith(opacity: value.clamp(0, 1).toDouble()), + 'rotate' => copyWith(rotate: value), + 'rotateX' => copyWith(rotateX: value), + 'rotateY' => copyWith(rotateY: value), + 'spin' => copyWith(spin: value), + _ => throw ArgumentError.value(property, 'property'), + }; + + @override + List get props => [ + translateX, + translateY, + scale, + opacity, + rotate, + rotateX, + rotateY, + perspective, + originX, + originY, + durationMs, + curve, + spin, + spinRampMs, + ]; + + /// Creates a [MotionModel] from JSON. + factory MotionModel.fromJson(Map json) => _$MotionModelFromJson(json); + + @override + Map toJson() => _$MotionModelOmitDefaults(this, _$MotionModelToJson(this)); +} diff --git a/lib/src/api/models/motion.g.dart b/lib/src/api/models/motion.g.dart new file mode 100644 index 00000000..4f36a89b --- /dev/null +++ b/lib/src/api/models/motion.g.dart @@ -0,0 +1,79 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'motion.dart'; + +// ************************************************************************** +// JsonDefaultsGenerator +// ************************************************************************** + +Map _$MotionModelOmitDefaults( + MotionModel instance, + Map json, +) { + if (isJsonDefault(instance.translateX, 0)) json.remove('translateX'); + if (isJsonDefault(instance.translateY, 0)) json.remove('translateY'); + if (isJsonDefault(instance.scale, 1)) json.remove('scale'); + if (isJsonDefault(instance.opacity, 1)) json.remove('opacity'); + if (isJsonDefault(instance.rotate, 0)) json.remove('rotate'); + if (isJsonDefault(instance.rotateX, 0)) json.remove('rotateX'); + if (isJsonDefault(instance.rotateY, 0)) json.remove('rotateY'); + if (isJsonDefault(instance.perspective, 1)) json.remove('perspective'); + if (isJsonDefault(instance.originX, 0)) json.remove('originX'); + if (isJsonDefault(instance.originY, 0)) json.remove('originY'); + if (isJsonDefault(instance.durationMs, 0)) json.remove('durationMs'); + if (isJsonDefault(instance.curve, MotionCurve.easeInOut)) + json.remove('curve'); + if (isJsonDefault(instance.spin, 0)) json.remove('spin'); + if (isJsonDefault(instance.spinRampMs, 800)) json.remove('spinRampMs'); + return json; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +MotionModel _$MotionModelFromJson(Map json) => MotionModel( + translateX: (json['translateX'] as num?)?.toDouble() ?? 0, + translateY: (json['translateY'] as num?)?.toDouble() ?? 0, + scale: (json['scale'] as num?)?.toDouble() ?? 1, + opacity: (json['opacity'] as num?)?.toDouble() ?? 1, + rotate: (json['rotate'] as num?)?.toDouble() ?? 0, + rotateX: (json['rotateX'] as num?)?.toDouble() ?? 0, + rotateY: (json['rotateY'] as num?)?.toDouble() ?? 0, + perspective: (json['perspective'] as num?)?.toDouble() ?? 1, + originX: (json['originX'] as num?)?.toDouble() ?? 0, + originY: (json['originY'] as num?)?.toDouble() ?? 0, + durationMs: (json['durationMs'] as num?)?.toInt() ?? 0, + curve: + $enumDecodeNullable(_$MotionCurveEnumMap, json['curve']) ?? + MotionCurve.easeInOut, + spin: (json['spin'] as num?)?.toDouble() ?? 0, + spinRampMs: (json['spinRampMs'] as num?)?.toInt() ?? 800, +); + +Map _$MotionModelToJson(MotionModel instance) => + { + 'translateX': instance.translateX, + 'translateY': instance.translateY, + 'scale': instance.scale, + 'opacity': instance.opacity, + 'rotate': instance.rotate, + 'rotateX': instance.rotateX, + 'rotateY': instance.rotateY, + 'perspective': instance.perspective, + 'originX': instance.originX, + 'originY': instance.originY, + 'durationMs': instance.durationMs, + 'curve': _$MotionCurveEnumMap[instance.curve]!, + 'spin': instance.spin, + 'spinRampMs': instance.spinRampMs, + }; + +const _$MotionCurveEnumMap = { + MotionCurve.linear: 'linear', + MotionCurve.easeInOut: 'easeInOut', + MotionCurve.easeOut: 'easeOut', + MotionCurve.easeIn: 'easeIn', + MotionCurve.easeOutBack: 'easeOutBack', + MotionCurve.easeOutCubic: 'easeOutCubic', +}; diff --git a/lib/src/api/models/trigger.dart b/lib/src/api/models/trigger.dart index 8b75dbdd..face0489 100644 --- a/lib/src/api/models/trigger.dart +++ b/lib/src/api/models/trigger.dart @@ -47,7 +47,36 @@ enum TriggerType { /// Triggered when the variable named by [TriggerModel.variable] changes, /// while the screen it is set on shows. - valueChanged; + valueChanged, + + /// Triggered over and over while the canvas is shown, every + /// [TriggerModel.intervalMs]. + timer, + + /// Triggered when a key is pressed while the canvas is shown. The key is + /// [TriggerModel.key], a logical key label such as `Arrow Right`, `Enter` + /// or `A`. + keyPress, + + /// Triggered when the user swipes the target to the left. + swipeLeft, + + /// Triggered when the user swipes the target to the right. + swipeRight, + + /// Triggered when the user swipes the target up. + swipeUp, + + /// Triggered when the user swipes the target down. + swipeDown; + + /// The swipe triggers. + static const List swipes = [ + TriggerType.swipeLeft, + TriggerType.swipeRight, + TriggerType.swipeUp, + TriggerType.swipeDown, + ]; /// Displayable string representation of the trigger type. String get prettify { @@ -78,6 +107,18 @@ enum TriggerType { return 'Load'; case TriggerType.valueChanged: return 'Value Changed'; + case TriggerType.timer: + return 'Timer'; + case TriggerType.keyPress: + return 'Key Press'; + case TriggerType.swipeLeft: + return 'Swipe Left'; + case TriggerType.swipeRight: + return 'Swipe Right'; + case TriggerType.swipeUp: + return 'Swipe Up'; + case TriggerType.swipeDown: + return 'Swipe Down'; } } } @@ -97,21 +138,41 @@ class TriggerModel with Equatable, SerializableMixin { /// For [TriggerType.valueChanged], the name of the variable to watch. final String? variable; + /// How often a [TriggerType.timer] fires, in milliseconds. + final int? intervalMs; + + /// The key a [TriggerType.keyPress] listens for, as a logical key label + /// (`Arrow Right`, `Enter`, `Space`, `A`). + final String? key; + /// Creates a [TriggerModel] with given data. - TriggerModel({required this.type, this.debounceMs = 0, this.variable}); + TriggerModel({ + required this.type, + this.debounceMs = 0, + this.variable, + this.intervalMs, + this.key, + }); + + /// The interval of a timer trigger; one second when unset. + int get effectiveIntervalMs => (intervalMs ?? 1000).clamp(16, 86400000); @override - List get props => [type, debounceMs, variable]; + List get props => [type, debounceMs, variable, intervalMs, key]; /// Duplicate this [TriggerModel] with given data overrides. TriggerModel copyWith({ TriggerType? type, int? debounceMs, String? variable, + int? intervalMs, + String? key, }) => TriggerModel( type: type ?? this.type, debounceMs: debounceMs ?? this.debounceMs, variable: variable ?? this.variable, + intervalMs: intervalMs ?? this.intervalMs, + key: key ?? this.key, ); /// Factory constructor for creating a new [TriggerModel] instance from diff --git a/lib/src/api/models/trigger.g.dart b/lib/src/api/models/trigger.g.dart index d522deee..dcfb7065 100644 --- a/lib/src/api/models/trigger.g.dart +++ b/lib/src/api/models/trigger.g.dart @@ -22,6 +22,8 @@ TriggerModel _$TriggerModelFromJson(Map json) => TriggerModel( type: $enumDecode(_$TriggerTypeEnumMap, json['type']), debounceMs: (json['debounceMs'] as num?)?.toInt() ?? 0, variable: json['variable'] as String?, + intervalMs: (json['intervalMs'] as num?)?.toInt(), + key: json['key'] as String?, ); Map _$TriggerModelToJson(TriggerModel instance) => @@ -29,6 +31,8 @@ Map _$TriggerModelToJson(TriggerModel instance) => 'type': _$TriggerTypeEnumMap[instance.type]!, 'debounceMs': instance.debounceMs, 'variable': ?instance.variable, + 'intervalMs': ?instance.intervalMs, + 'key': ?instance.key, }; const _$TriggerTypeEnumMap = { @@ -45,4 +49,10 @@ const _$TriggerTypeEnumMap = { TriggerType.pointerUp: 'pointerUp', TriggerType.load: 'load', TriggerType.valueChanged: 'valueChanged', + TriggerType.timer: 'timer', + TriggerType.keyPress: 'keyPress', + TriggerType.swipeLeft: 'swipeLeft', + TriggerType.swipeRight: 'swipeRight', + TriggerType.swipeUp: 'swipeUp', + TriggerType.swipeDown: 'swipeDown', }; diff --git a/lib/src/api/nodes/accordion_node.g.dart b/lib/src/api/nodes/accordion_node.g.dart index 77f4f620..72bbd8b1 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$AccordionNodeToJson( @@ -214,6 +217,7 @@ Map _$AccordionNodeToJson( 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'rowColumnType': _$RowColumnTypeEnumMap[instance.rowColumnType]!, 'mainAxisAlignment': _$MainAxisAlignmentCEnumMap[instance.mainAxisAlignment]!, diff --git a/lib/src/api/nodes/app_bar_node.g.dart b/lib/src/api/nodes/app_bar_node.g.dart index de0fa8ba..d2ecc1c8 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$AppBarNodeToJson(AppBarNode instance) => @@ -196,6 +199,7 @@ Map _$AppBarNodeToJson(AppBarNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), '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..d69ef2c9 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -271,6 +274,7 @@ Map _$AutoPlaceholderNodeToJson( 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, diff --git a/lib/src/api/nodes/badge_node.g.dart b/lib/src/api/nodes/badge_node.g.dart index 675e8d90..df487eca 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$BadgeNodeToJson(BadgeNode instance) => { @@ -197,6 +200,7 @@ Map _$BadgeNodeToJson(BadgeNode instance) => { 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), 'childID': ?instance.childID, diff --git a/lib/src/api/nodes/base_node.dart b/lib/src/api/nodes/base_node.dart index f37c42ec..8392ec5f 100644 --- a/lib/src/api/nodes/base_node.dart +++ b/lib/src/api/nodes/base_node.dart @@ -393,6 +393,10 @@ abstract class BaseNode /// should be included and accounted for or not. bool enabled; + /// How the node moves: a transform drawn over its layout, transitions and + /// spin. Null for a node that doesn't move. See [MotionModel]. + MotionModel? motion; + /// Whether the node is sized in fraction of the parent's size. bool get isFractionallySized => widthFactor != null || heightFactor != null; diff --git a/lib/src/api/nodes/button_node.g.dart b/lib/src/api/nodes/button_node.g.dart index 443ae349..15529e74 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$ButtonNodeToJson(ButtonNode instance) => @@ -207,6 +210,7 @@ Map _$ButtonNodeToJson(ButtonNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), }; diff --git a/lib/src/api/nodes/canvas_node.dart b/lib/src/api/nodes/canvas_node.dart index 9634c1f0..9ccbc571 100644 --- a/lib/src/api/nodes/canvas_node.dart +++ b/lib/src/api/nodes/canvas_node.dart @@ -46,6 +46,8 @@ class CanvasNode extends ParentNode List get triggerTypes => [ TriggerType.load, TriggerType.valueChanged, + TriggerType.timer, + TriggerType.keyPress, ]; @override diff --git a/lib/src/api/nodes/canvas_node.g.dart b/lib/src/api/nodes/canvas_node.g.dart index 70aeb784..4df6296f 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -325,6 +328,7 @@ Map _$CanvasNodeToJson( 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, diff --git a/lib/src/api/nodes/checkbox_node.g.dart b/lib/src/api/nodes/checkbox_node.g.dart index 1ba0fa1d..4fa5ed3b 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$CheckboxNodeToJson(CheckboxNode instance) => @@ -202,6 +205,7 @@ Map _$CheckboxNodeToJson(CheckboxNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), 'value': ?instance.value, diff --git a/lib/src/api/nodes/chip_node.g.dart b/lib/src/api/nodes/chip_node.g.dart index 4b930f59..d4f683ff 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$ChipNodeToJson(ChipNode instance) => { @@ -195,6 +198,7 @@ Map _$ChipNodeToJson(ChipNode instance) => { 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), '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..4414dc2d 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$DividerNodeToJson(DividerNode instance) => @@ -185,6 +188,7 @@ Map _$DividerNodeToJson(DividerNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), }; diff --git a/lib/src/api/nodes/dropdown_node.g.dart b/lib/src/api/nodes/dropdown_node.g.dart index 8739ea00..3c29e096 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$DropdownNodeToJson(DropdownNode instance) => @@ -213,6 +216,7 @@ Map _$DropdownNodeToJson(DropdownNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), 'value': ?instance.value, diff --git a/lib/src/api/nodes/embedded_video_node.g.dart b/lib/src/api/nodes/embedded_video_node.g.dart index 4b4ef740..a7639c04 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$EmbeddedVideoNodeToJson(EmbeddedVideoNode instance) => @@ -222,6 +225,7 @@ Map _$EmbeddedVideoNodeToJson(EmbeddedVideoNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), '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..f880acdb 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] 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(), @@ -240,6 +243,7 @@ Map _$ExpansionTileNodeToJson( 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'rowColumnType': _$RowColumnTypeEnumMap[instance.rowColumnType]!, 'mainAxisAlignment': _$MainAxisAlignmentCEnumMap[instance.mainAxisAlignment]!, diff --git a/lib/src/api/nodes/external_component_node.g.dart b/lib/src/api/nodes/external_component_node.g.dart index 7eeceb4a..9f4f0326 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$ExternalComponentNodeToJson( @@ -185,6 +188,7 @@ Map _$ExternalComponentNodeToJson( 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), }; 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..4c2e9623 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$FloatingActionButtonNodeToJson( @@ -206,6 +209,7 @@ Map _$FloatingActionButtonNodeToJson( 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), '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..a9031d16 100644 --- a/lib/src/api/nodes/frame_node.dart +++ b/lib/src/api/nodes/frame_node.dart @@ -107,6 +107,7 @@ class FrameNode extends DefaultShapeNode List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + ...TriggerType.swipes, ]; /// 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..d1f5b1d7 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -295,6 +298,7 @@ Map _$FrameNodeToJson(FrameNode instance) => { 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, diff --git a/lib/src/api/nodes/freeform_placeholder_node.g.dart b/lib/src/api/nodes/freeform_placeholder_node.g.dart index 1f9d5453..4aac8593 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -245,6 +248,7 @@ Map _$FreeformPlaceholderNodeToJson( 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, diff --git a/lib/src/api/nodes/grid_view_node.g.dart b/lib/src/api/nodes/grid_view_node.g.dart index eb9c6485..86780e93 100644 --- a/lib/src/api/nodes/grid_view_node.g.dart +++ b/lib/src/api/nodes/grid_view_node.g.dart @@ -11,6 +11,10 @@ Map _$GridViewNodeOmitDefaults( Map json, ) { if (isJsonDefault(instance.ephemeral, false)) json.remove('ephemeral'); + if (isJsonDefault(instance.allowedTypes, [])) + json.remove('allowedTypes'); + if (isJsonDefault(instance.deniedTypes, [])) + json.remove('deniedTypes'); if (isJsonDefault(instance.visible, true)) json.remove('visible'); if (excludeConstraintsIf(instance)) json.remove('constraints'); if (isJsonDefault(instance.constraints, const BoxConstraintsModel())) @@ -242,16 +246,23 @@ GridViewNode _$GridViewNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false - ..deniedTypes = (json['deniedTypes'] as List) - .map((e) => e as String) - .toList() + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) + ..deniedTypes = + (json['deniedTypes'] as List?) + ?.map((e) => e as String) + .toList() ?? + [] ..maxAllowedSize = json['maxAllowedSize'] == null ? null : SizeC.fromJson(json['maxAllowedSize'] as Map) ..ephemeral = json['ephemeral'] as bool? ?? false - ..allowedTypes = (json['allowedTypes'] as List) - .map((e) => e as String) - .toList() + ..allowedTypes = + (json['allowedTypes'] as List?) + ?.map((e) => e as String) + .toList() ?? + [] ..isScrollable = json['isScrollable'] as bool ..useFlutterListView = json['useFlutterListView'] as bool ..paging = @@ -266,7 +277,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 +294,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(), @@ -296,6 +307,7 @@ Map _$GridViewNodeToJson(GridViewNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'deniedTypes': instance.deniedTypes, 'maxAllowedSize': ?instance.maxAllowedSize?.toJson(), diff --git a/lib/src/api/nodes/icon_node.g.dart b/lib/src/api/nodes/icon_node.g.dart index 59174871..3e6969d7 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$IconNodeToJson(IconNode instance) => { @@ -205,6 +208,7 @@ Map _$IconNodeToJson(IconNode instance) => { 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, 'isMask': instance.isMask, diff --git a/lib/src/api/nodes/list_tile_node.g.dart b/lib/src/api/nodes/list_tile_node.g.dart index 326836c6..3bf4e714 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$ListTileNodeToJson(ListTileNode instance) => @@ -212,6 +215,7 @@ Map _$ListTileNodeToJson(ListTileNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'type': instance.type, 'leading': ?instance.leading, diff --git a/lib/src/api/nodes/list_view_node.dart b/lib/src/api/nodes/list_view_node.dart index e1cd40d6..b287f07e 100644 --- a/lib/src/api/nodes/list_view_node.dart +++ b/lib/src/api/nodes/list_view_node.dart @@ -120,7 +120,14 @@ class ListViewNode extends SinglePlaceholderNode } /// Creates a [ListViewNode] from a JSON object. - factory ListViewNode.fromJson(Map json) => _$ListViewNodeFromJson(json); + /// + /// A list always scrolls with a Flutter list view, so JSON written by hand + /// can leave those flags out. + factory ListViewNode.fromJson(Map json) => _$ListViewNodeFromJson({ + 'isScrollable': true, + 'useFlutterListView': true, + ...json, + }); @override Map toJson() => _$ListViewNodeOmitDefaults(this, _$ListViewNodeToJson(this)); @@ -187,6 +194,13 @@ class ListViewProperties extends CustomProperties with FieldsHolder { /// items, if enabled. DividerProperties dividerProperties; + /// Whether the items are stacked on top of each other, like a deck of + /// cards or records in a crate, instead of laid out one after another. + /// The first item is drawn on top. A stacked list doesn't scroll; items + /// are placed at the top center of the list and are usually moved apart + /// with motion bound to their index (see [MotionModel]). + bool stacked; + /// Creates a new [ListViewProperties] instance. ListViewProperties({ this.itemCount, @@ -195,6 +209,7 @@ class ListViewProperties extends CustomProperties with FieldsHolder { this.separator = ListItemSeparator.space, this.separatorSpacing = 0, this.hasSeparator = false, + this.stacked = false, DividerProperties? dividerProperties, }) : dividerProperties = dividerProperties ?? DividerProperties(); @@ -214,9 +229,11 @@ class ListViewProperties extends CustomProperties with FieldsHolder { ColorRGBA? dividerColor, double? dividerSpacing, bool? hasSeparator, + bool? stacked, DividerProperties? dividerProperties, }) { return ListViewProperties( + stacked: stacked ?? this.stacked, itemCount: itemCount ?? this.itemCount, itemExtent: itemExtent ?? this.itemExtent, cacheExtent: cacheExtent ?? this.cacheExtent, @@ -230,6 +247,14 @@ class ListViewProperties extends CustomProperties with FieldsHolder { @override FieldsMap generateFields() => { ...super.generateFields(), + 'stacked': BoolFieldAccess( + 'Stacked', + 'Draw the items on top of each other, the first on top, instead of ' + 'one after another.', + () => stacked, + (value) => stacked = value, + requiresLayout: true, + ), 'hasSeparator': BoolFieldAccess( 'Has Separator', 'Whether to use a separator between list view items.', @@ -278,6 +303,7 @@ class ListViewProperties extends CustomProperties with FieldsHolder { separator, separatorSpacing, hasSeparator, + stacked, dividerProperties, ]; } diff --git a/lib/src/api/nodes/list_view_node.g.dart b/lib/src/api/nodes/list_view_node.g.dart index 2dce408d..80405de1 100644 --- a/lib/src/api/nodes/list_view_node.g.dart +++ b/lib/src/api/nodes/list_view_node.g.dart @@ -11,6 +11,10 @@ Map _$ListViewNodeOmitDefaults( Map json, ) { if (isJsonDefault(instance.ephemeral, false)) json.remove('ephemeral'); + if (isJsonDefault(instance.allowedTypes, [])) + json.remove('allowedTypes'); + if (isJsonDefault(instance.deniedTypes, [])) + json.remove('deniedTypes'); if (isJsonDefault(instance.visible, true)) json.remove('visible'); if (excludeConstraintsIf(instance)) json.remove('constraints'); if (isJsonDefault(instance.constraints, const BoxConstraintsModel())) @@ -80,6 +84,7 @@ Map _$ListViewPropertiesOmitDefaults( if (isJsonDefault(instance.separator, ListItemSeparator.space)) json.remove('separator'); if (isJsonDefault(instance.hasSeparator, false)) json.remove('hasSeparator'); + if (isJsonDefault(instance.stacked, false)) json.remove('stacked'); return json; } @@ -221,16 +226,23 @@ ListViewNode _$ListViewNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false - ..deniedTypes = (json['deniedTypes'] as List) - .map((e) => e as String) - .toList() + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) + ..deniedTypes = + (json['deniedTypes'] as List?) + ?.map((e) => e as String) + .toList() ?? + [] ..maxAllowedSize = json['maxAllowedSize'] == null ? null : SizeC.fromJson(json['maxAllowedSize'] as Map) ..ephemeral = json['ephemeral'] as bool? ?? false - ..allowedTypes = (json['allowedTypes'] as List) - .map((e) => e as String) - .toList() + ..allowedTypes = + (json['allowedTypes'] as List?) + ?.map((e) => e as String) + .toList() ?? + [] ..isScrollable = json['isScrollable'] as bool ..useFlutterListView = json['useFlutterListView'] as bool ..paging = @@ -245,7 +257,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 +274,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(), @@ -275,6 +287,7 @@ Map _$ListViewNodeToJson(ListViewNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'deniedTypes': instance.deniedTypes, 'maxAllowedSize': ?instance.maxAllowedSize?.toJson(), @@ -361,6 +374,7 @@ ListViewProperties _$ListViewPropertiesFromJson(Map json) => ListViewProperties( ListItemSeparator.space, separatorSpacing: (json['separatorSpacing'] as num?)?.toDouble() ?? 0, hasSeparator: json['hasSeparator'] as bool? ?? false, + stacked: json['stacked'] as bool? ?? false, dividerProperties: json['dividerProperties'] == null ? null : DividerProperties.fromJson(json['dividerProperties'] as Map), @@ -375,6 +389,7 @@ Map _$ListViewPropertiesToJson(ListViewProperties instance) => 'separator': _$ListItemSeparatorEnumMap[instance.separator]!, 'hasSeparator': instance.hasSeparator, 'dividerProperties': instance.dividerProperties.toJson(), + 'stacked': instance.stacked, }; const _$ListItemSeparatorEnumMap = { diff --git a/lib/src/api/nodes/loading_indicator_node.g.dart b/lib/src/api/nodes/loading_indicator_node.g.dart index 6e5dadc3..9c4c6d81 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$LoadingIndicatorNodeToJson( @@ -196,6 +199,7 @@ Map _$LoadingIndicatorNodeToJson( 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), '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..d91db4d9 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$NavigationBarNodeToJson(NavigationBarNode instance) => @@ -220,6 +223,7 @@ Map _$NavigationBarNodeToJson(NavigationBarNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'currentIndex': instance.currentIndex, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/page_view_node.g.dart b/lib/src/api/nodes/page_view_node.g.dart index bdc21484..61f75975 100644 --- a/lib/src/api/nodes/page_view_node.g.dart +++ b/lib/src/api/nodes/page_view_node.g.dart @@ -11,6 +11,10 @@ Map _$PageViewNodeOmitDefaults( Map json, ) { if (isJsonDefault(instance.ephemeral, false)) json.remove('ephemeral'); + if (isJsonDefault(instance.allowedTypes, [])) + json.remove('allowedTypes'); + if (isJsonDefault(instance.deniedTypes, [])) + json.remove('deniedTypes'); if (isJsonDefault(instance.visible, true)) json.remove('visible'); if (excludeConstraintsIf(instance)) json.remove('constraints'); if (isJsonDefault(instance.constraints, const BoxConstraintsModel())) @@ -195,13 +199,20 @@ PageViewNode _$PageViewNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..ephemeral = json['ephemeral'] as bool? ?? false - ..allowedTypes = (json['allowedTypes'] as List) - .map((e) => e as String) - .toList() - ..deniedTypes = (json['deniedTypes'] as List) - .map((e) => e as String) - .toList() + ..allowedTypes = + (json['allowedTypes'] as List?) + ?.map((e) => e as String) + .toList() ?? + [] + ..deniedTypes = + (json['deniedTypes'] as List?) + ?.map((e) => e as String) + .toList() ?? + [] ..maxAllowedSize = json['maxAllowedSize'] == null ? null : SizeC.fromJson(json['maxAllowedSize'] as Map) @@ -241,6 +252,7 @@ Map _$PageViewNodeToJson(PageViewNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'clipsContent': instance.clipsContent, 'ephemeral': instance.ephemeral, diff --git a/lib/src/api/nodes/placeholder_node.g.dart b/lib/src/api/nodes/placeholder_node.g.dart index da90e37b..6e00c179 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$PlaceholderNodeToJson(PlaceholderNode instance) => @@ -174,6 +177,7 @@ Map _$PlaceholderNodeToJson(PlaceholderNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'leadingAsset': ?instance.leadingAsset, 'text': ?instance.text, diff --git a/lib/src/api/nodes/progressbar_node.g.dart b/lib/src/api/nodes/progressbar_node.g.dart index faaf5a17..e7116687 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$ProgressBarNodeToJson(ProgressBarNode instance) => @@ -193,6 +196,7 @@ Map _$ProgressBarNodeToJson(ProgressBarNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), 'currentValue': instance.currentValue, diff --git a/lib/src/api/nodes/radio_node.g.dart b/lib/src/api/nodes/radio_node.g.dart index d1ff5b62..5a047754 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] 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(), @@ -194,6 +197,7 @@ Map _$RadioNodeToJson(RadioNode instance) => { 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), 'value': instance.value, diff --git a/lib/src/api/nodes/rectangle_node.dart b/lib/src/api/nodes/rectangle_node.dart index 4875b198..d5779f42 100644 --- a/lib/src/api/nodes/rectangle_node.dart +++ b/lib/src/api/nodes/rectangle_node.dart @@ -69,6 +69,7 @@ class RectangleNode extends DefaultShapeNode with CornerMixin, ClipMixin { List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + ...TriggerType.swipes, ]; @override diff --git a/lib/src/api/nodes/rectangle_node.g.dart b/lib/src/api/nodes/rectangle_node.g.dart index 3e3fc760..40589b58 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] 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(), @@ -248,6 +251,7 @@ Map _$RectangleNodeToJson(RectangleNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, 'isMask': instance.isMask, diff --git a/lib/src/api/nodes/row_column_node.g.dart b/lib/src/api/nodes/row_column_node.g.dart index 4385e68c..20e5f40a 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..inkWell = json['inkWell'] == null ? null : InkWellModel.fromJson( @@ -307,6 +310,7 @@ Map _$RowColumnNodeToJson( 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, diff --git a/lib/src/api/nodes/segmented_button_node.g.dart b/lib/src/api/nodes/segmented_button_node.g.dart index 7229d2d8..776e7253 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$SegmentedButtonNodeToJson( @@ -212,6 +215,7 @@ Map _$SegmentedButtonNodeToJson( 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), }; diff --git a/lib/src/api/nodes/single_placeholder_node.dart b/lib/src/api/nodes/single_placeholder_node.dart index 2cfa6ca2..0d7c164d 100644 --- a/lib/src/api/nodes/single_placeholder_node.dart +++ b/lib/src/api/nodes/single_placeholder_node.dart @@ -34,12 +34,14 @@ class SinglePlaceholderNode extends SceneNode /// The node types that are allowed to be inside this [SinglePlaceholderNode]. /// Example: 'APP_BAR' for AppBarNode. /// When empty, allow all types. + @JsonKey(defaultValue: []) List allowedTypes; /// The node types that are not allowed to be inside this /// [SinglePlaceholderNode]. /// Example: "TEXT" for TextNode. /// When empty, allow all types. + @JsonKey(defaultValue: []) List deniedTypes; /// If not null, this [SinglePlaceholderNode] will only accept nodes that do diff --git a/lib/src/api/nodes/single_placeholder_node.g.dart b/lib/src/api/nodes/single_placeholder_node.g.dart index d8ce6f51..c6a68e81 100644 --- a/lib/src/api/nodes/single_placeholder_node.g.dart +++ b/lib/src/api/nodes/single_placeholder_node.g.dart @@ -11,6 +11,10 @@ Map _$SinglePlaceholderNodeOmitDefaults( Map json, ) { if (isJsonDefault(instance.ephemeral, false)) json.remove('ephemeral'); + if (isJsonDefault(instance.allowedTypes, [])) + json.remove('allowedTypes'); + if (isJsonDefault(instance.deniedTypes, [])) + json.remove('deniedTypes'); if (isJsonDefault(instance.visible, true)) json.remove('visible'); if (excludeConstraintsIf(instance)) json.remove('constraints'); if (isJsonDefault(instance.constraints, const BoxConstraintsModel())) @@ -56,12 +60,16 @@ Map _$SinglePlaceholderNodeOmitDefaults( SinglePlaceholderNode _$SinglePlaceholderNodeFromJson(Map json) => SinglePlaceholderNode( - allowedTypes: (json['allowedTypes'] as List) - .map((e) => e as String) - .toList(), - deniedTypes: (json['deniedTypes'] as List) - .map((e) => e as String) - .toList(), + allowedTypes: + (json['allowedTypes'] as List?) + ?.map((e) => e as String) + .toList() ?? + [], + deniedTypes: + (json['deniedTypes'] as List?) + ?.map((e) => e as String) + .toList() ?? + [], maxAllowedSize: json['maxAllowedSize'] == null ? null : SizeC.fromJson(json['maxAllowedSize'] as Map), @@ -155,6 +163,9 @@ SinglePlaceholderNode _$SinglePlaceholderNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$SinglePlaceholderNodeToJson( @@ -190,6 +201,7 @@ Map _$SinglePlaceholderNodeToJson( 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'children': instance.children, 'type': instance.type, 'ephemeral': instance.ephemeral, diff --git a/lib/src/api/nodes/slider_node.g.dart b/lib/src/api/nodes/slider_node.g.dart index 93ef55f9..0ea4e25d 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$SliderNodeToJson(SliderNode instance) => @@ -231,6 +234,7 @@ Map _$SliderNodeToJson(SliderNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'value': instance.value, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/spacer_node.g.dart b/lib/src/api/nodes/spacer_node.g.dart index 9d7b529c..75e02a14 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$SpacerNodeToJson(SpacerNode instance) => @@ -140,6 +143,7 @@ Map _$SpacerNodeToJson(SpacerNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), '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..50993fe6 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$SwitchNodeToJson(SwitchNode instance) => @@ -204,6 +207,7 @@ Map _$SwitchNodeToJson(SwitchNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'value': instance.value, 'properties': instance.properties.toJson(), diff --git a/lib/src/api/nodes/tab_bar_node.g.dart b/lib/src/api/nodes/tab_bar_node.g.dart index b93405cc..49449d7e 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..scrollDirection = $enumDecode(_$AxisCEnumMap, json['scrollDirection']) ..reverse = json['reverse'] as bool ..primary = json['primary'] as bool @@ -247,6 +250,7 @@ Map _$TabBarNodeToJson(TabBarNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'isScrollable': instance.isScrollable, 'scrollDirection': _$AxisCEnumMap[instance.scrollDirection]!, 'reverse': instance.reverse, diff --git a/lib/src/api/nodes/text_field_node.g.dart b/lib/src/api/nodes/text_field_node.g.dart index 1ca7bd8b..3d1a9d09 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$TextFieldNodeToJson(TextFieldNode instance) => @@ -228,6 +231,7 @@ Map _$TextFieldNodeToJson(TextFieldNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), 'initialText': instance.initialText, diff --git a/lib/src/api/nodes/text_node.g.dart b/lib/src/api/nodes/text_node.g.dart index 01949d07..a70cdcdc 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$TextNodeToJson(TextNode instance) => { @@ -261,6 +264,7 @@ Map _$TextNodeToJson(TextNode instance) => { 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'opacity': instance.opacity, 'blendMode': _$BlendModeCEnumMap[instance.blendMode]!, 'isMask': instance.isMask, diff --git a/lib/src/api/nodes/variance_node.g.dart b/lib/src/api/nodes/variance_node.g.dart index be7a515d..bbeb6332 100644 --- a/lib/src/api/nodes/variance_node.g.dart +++ b/lib/src/api/nodes/variance_node.g.dart @@ -13,6 +13,10 @@ Map _$VarianceNodeOmitDefaults( if (isJsonDefault(instance.properties, const VarianceProperties())) json.remove('properties'); if (isJsonDefault(instance.ephemeral, false)) json.remove('ephemeral'); + if (isJsonDefault(instance.allowedTypes, [])) + json.remove('allowedTypes'); + if (isJsonDefault(instance.deniedTypes, [])) + json.remove('deniedTypes'); if (isJsonDefault(instance.visible, true)) json.remove('visible'); if (excludeConstraintsIf(instance)) json.remove('constraints'); if (isJsonDefault(instance.constraints, const BoxConstraintsModel())) @@ -168,13 +172,20 @@ VarianceNode _$VarianceNodeFromJson(Map json) => {} ..tooltip = json['tooltip'] as String? ?? '' ..zoomable = json['zoomable'] as bool? ?? false + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..ephemeral = json['ephemeral'] as bool? ?? false - ..allowedTypes = (json['allowedTypes'] as List) - .map((e) => e as String) - .toList() - ..deniedTypes = (json['deniedTypes'] as List) - .map((e) => e as String) - .toList() + ..allowedTypes = + (json['allowedTypes'] as List?) + ?.map((e) => e as String) + .toList() ?? + [] + ..deniedTypes = + (json['deniedTypes'] as List?) + ?.map((e) => e as String) + .toList() ?? + [] ..type = json['type'] as String; Map _$VarianceNodeToJson(VarianceNode instance) => @@ -209,6 +220,7 @@ Map _$VarianceNodeToJson(VarianceNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'ephemeral': instance.ephemeral, 'allowedTypes': instance.allowedTypes, 'deniedTypes': instance.deniedTypes, diff --git a/lib/src/api/nodes/web_view_node.g.dart b/lib/src/api/nodes/web_view_node.g.dart index 48c3c29e..d7e06830 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 + ..motion = json['motion'] == null + ? null + : MotionModel.fromJson(json['motion'] as Map) ..type = json['type'] as String; Map _$WebViewNodeToJson(WebViewNode instance) => @@ -185,6 +188,7 @@ Map _$WebViewNodeToJson(WebViewNode instance) => 'tooltip': instance.tooltip, 'zoomable': instance.zoomable, 'enabled': instance.enabled, + 'motion': ?instance.motion?.toJson(), 'type': instance.type, 'properties': instance.properties.toJson(), }; diff --git a/test/placeholder_json_defaults_test.dart b/test/placeholder_json_defaults_test.dart new file mode 100644 index 00000000..c2e77e52 --- /dev/null +++ b/test/placeholder_json_defaults_test.dart @@ -0,0 +1,21 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('A list parses from hand-written JSON with only what it needs', () { + for (final type in ['listView']) { + final node = NodeJsonConverter().fromJson({ + 'type': type, + 'id': 'x', + 'name': 'n', + 'basicBoxLocal': [0, 0, 100, 100], + 'children': ['item'], + 'properties': {}, + }) as SinglePlaceholderNode; + expect(node.allowedTypes, isEmpty, reason: type); + expect(node.deniedTypes, isEmpty, reason: type); + expect((node as ListViewNode).isScrollable, isTrue); + expect(node.useFlutterListView, isTrue); + } + }); +} From 0fdcda894f0a3f595aa452530431ee8d54e184a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 03:21:04 +0000 Subject: [PATCH 2/9] Wrapping lists, bindable style properties, spelled-out padding - ListViewProperties.wrap with spacing, run spacing and alignment. - effectsVisible, strokeWeight, cornerRadius and fontWeight are property variables Set value actions can change. - EdgeInsetsModel reads {left, top, right, bottom} and {horizontal, vertical}. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013oMokUGNW6txknJs7xxTik --- lib/src/api/mixins.dart | 26 +++++++-- lib/src/api/models/edge_insets.dart | 21 +++++++ lib/src/api/nodes/list_view_node.dart | 74 +++++++++++++++++++++++++ lib/src/api/nodes/list_view_node.g.dart | 24 ++++++++ test/edge_insets_json_test.dart | 31 +++++++++++ test/list_wrap_test.dart | 43 ++++++++++++++ test/style_property_variables_test.dart | 55 ++++++++++++++++++ 7 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 test/edge_insets_json_test.dart create mode 100644 test/list_wrap_test.dart create mode 100644 test/style_property_variables_test.dart diff --git a/lib/src/api/mixins.dart b/lib/src/api/mixins.dart index b182ef92..513155f4 100644 --- a/lib/src/api/mixins.dart +++ b/lib/src/api/mixins.dart @@ -1045,8 +1045,9 @@ mixin PropertyVariableMixin on BaseNode { /// Properties of the node that can be used in actions or can be changed by /// an action. /// - /// Style properties (fill, stroke, opacity, width, height; a text's fill is - /// its color) can be set by + /// Style properties (fill, stroke, opacity, width, height, effectsVisible, + /// strokeWeight, cornerRadius; a text's fill is its color, and its + /// fontWeight a number from 100 to 900) can be set by /// conditions, bound to variables or formulas, or changed by a set value /// action, on any node that has them. late final List propertyVariables = [ @@ -1056,13 +1057,30 @@ mixin PropertyVariableMixin on BaseNode { PaintValue(name: 'fill', value: geometry.fills.firstOrNull), PaintValue(name: 'stroke', value: geometry.strokes.firstOrNull), ], - if (this case final TextNode text) + if (this case final TextNode text) ...[ PaintValue( name: 'fill', value: text.textMixedProps.firstOrNull?.fills.firstOrNull, ), - if (this case final BlendMixin blend) + IntValue( + name: 'fontWeight', + value: + ((text.textMixedProps.firstOrNull?.fontName.weight?.index ?? 3) + + 1) * + 100, + ), + ], + if (this case final BlendMixin blend) ...[ DoubleValue(name: 'opacity', value: blend.opacity), + BoolValue( + name: 'effectsVisible', + value: blend.effects.every((effect) => effect.visible), + ), + ], + if (this case final GeometryMixin geometry) + DoubleValue(name: 'strokeWeight', value: geometry.strokeWeight), + if (this case final CornerMixin corner) + DoubleValue(name: 'cornerRadius', value: corner.cornerRadius.tl.x), DoubleValue(name: 'width', value: basicBoxLocal.width), DoubleValue(name: 'height', value: basicBoxLocal.height), ]; diff --git a/lib/src/api/models/edge_insets.dart b/lib/src/api/models/edge_insets.dart index ebb5dc13..bb9f1e69 100644 --- a/lib/src/api/models/edge_insets.dart +++ b/lib/src/api/models/edge_insets.dart @@ -190,6 +190,18 @@ class EdgeInsetsModel with Equatable, DynamicSerializableMixin { /// Factory constructor for creating a new [EdgeInsetsModel] instance /// from JSON data. + static const Set _longSideKeys = { + 'left', + 'top', + 'right', + 'bottom', + 'horizontal', + 'vertical', + }; + + static double _side(Map json, String side, String axis) => + ((json[side] ?? json[axis] ?? 0) as num).toDouble(); + factory EdgeInsetsModel.fromJson(dynamic json) { return switch (json) { num all => EdgeInsetsModel.all(all.toDouble()), @@ -203,6 +215,15 @@ class EdgeInsetsModel with Equatable, DynamicSerializableMixin { right: right.toDouble(), bottom: bottom.toDouble(), ), + // Spelled-out sides, as Flutter names them; missing sides are 0. + Map sides + when sides.keys.any(_longSideKeys.contains) => + EdgeInsetsModel.only( + left: _side(sides, 'left', 'horizontal'), + top: _side(sides, 'top', 'vertical'), + right: _side(sides, 'right', 'horizontal'), + bottom: _side(sides, 'bottom', 'vertical'), + ), _ => _$EdgeInsetsModelFromJson(json), }; } diff --git a/lib/src/api/nodes/list_view_node.dart b/lib/src/api/nodes/list_view_node.dart index b287f07e..eedd6ba0 100644 --- a/lib/src/api/nodes/list_view_node.dart +++ b/lib/src/api/nodes/list_view_node.dart @@ -201,6 +201,33 @@ class ListViewProperties extends CustomProperties with FieldsHolder { /// with motion bound to their index (see [MotionModel]). bool stacked; + /// Whether the items flow onto further lines (runs) when they don't fit, + /// like Flutter's [Wrap], instead of scrolling in one line. The scroll + /// direction is the wrap direction: a horizontal list fills a line from + /// left to right, then starts a new line below; a vertical list fills a + /// column top to bottom, then starts a new column to the right. Items keep + /// their own size (they are not stretched to the list's cross extent). + /// Separators are not drawn; use [wrapSpacing] and [wrapRunSpacing]. + /// Ignored when [stacked] is true. + /// + /// A wrapping list shows every item, so it needs an item count or a data + /// source. It scrolls along the cross axis when its runs don't fit its box; + /// give it a shrink-wrap fit on the cross axis to grow with its runs. + bool wrap; + + /// The gap between two items of the same line when [wrap] is true. + double wrapSpacing; + + /// The gap between two lines (runs) when [wrap] is true. + double wrapRunSpacing; + + /// How the items of a line are placed along the wrap direction when [wrap] + /// is true. Maps to Flutter's [WrapAlignment] by name. + MainAxisAlignmentC wrapAlignment; + + /// Whether this list wraps its items: [wrap] is on and [stacked] is off. + bool get isWrapping => wrap && !stacked; + /// Creates a new [ListViewProperties] instance. ListViewProperties({ this.itemCount, @@ -210,6 +237,10 @@ class ListViewProperties extends CustomProperties with FieldsHolder { this.separatorSpacing = 0, this.hasSeparator = false, this.stacked = false, + this.wrap = false, + this.wrapSpacing = 0, + this.wrapRunSpacing = 0, + this.wrapAlignment = MainAxisAlignmentC.start, DividerProperties? dividerProperties, }) : dividerProperties = dividerProperties ?? DividerProperties(); @@ -230,10 +261,18 @@ class ListViewProperties extends CustomProperties with FieldsHolder { double? dividerSpacing, bool? hasSeparator, bool? stacked, + bool? wrap, + double? wrapSpacing, + double? wrapRunSpacing, + MainAxisAlignmentC? wrapAlignment, DividerProperties? dividerProperties, }) { return ListViewProperties( stacked: stacked ?? this.stacked, + wrap: wrap ?? this.wrap, + wrapSpacing: wrapSpacing ?? this.wrapSpacing, + wrapRunSpacing: wrapRunSpacing ?? this.wrapRunSpacing, + wrapAlignment: wrapAlignment ?? this.wrapAlignment, itemCount: itemCount ?? this.itemCount, itemExtent: itemExtent ?? this.itemExtent, cacheExtent: cacheExtent ?? this.cacheExtent, @@ -255,6 +294,37 @@ class ListViewProperties extends CustomProperties with FieldsHolder { (value) => stacked = value, requiresLayout: true, ), + 'wrap': BoolFieldAccess( + 'Wrap', + 'Flow the items onto further lines when they do not fit, instead of ' + 'scrolling in one line. Ignored when Stacked is on.', + () => wrap, + (value) => wrap = value, + requiresLayout: true, + ), + 'wrapSpacing': NumFieldAccess( + 'Wrap Spacing', + 'The gap between two items of the same line of a wrapping list.', + () => wrapSpacing, + (value) => wrapSpacing = value, + requiresLayout: true, + ), + 'wrapRunSpacing': NumFieldAccess( + 'Wrap Run Spacing', + 'The gap between two lines of a wrapping list.', + () => wrapRunSpacing, + (value) => wrapRunSpacing = value, + requiresLayout: true, + ), + 'wrapAlignment': EnumFieldAccess( + 'Wrap Alignment', + 'How the items of a line of a wrapping list are placed along it.', + () => wrapAlignment, + (value) => wrapAlignment = value, + defaultValue: () => MainAxisAlignmentC.start, + options: () => MainAxisAlignmentC.values, + requiresLayout: true, + ), 'hasSeparator': BoolFieldAccess( 'Has Separator', 'Whether to use a separator between list view items.', @@ -304,6 +374,10 @@ class ListViewProperties extends CustomProperties with FieldsHolder { separatorSpacing, hasSeparator, stacked, + wrap, + wrapSpacing, + wrapRunSpacing, + wrapAlignment, dividerProperties, ]; } diff --git a/lib/src/api/nodes/list_view_node.g.dart b/lib/src/api/nodes/list_view_node.g.dart index 80405de1..0ca48de4 100644 --- a/lib/src/api/nodes/list_view_node.g.dart +++ b/lib/src/api/nodes/list_view_node.g.dart @@ -85,6 +85,11 @@ Map _$ListViewPropertiesOmitDefaults( json.remove('separator'); if (isJsonDefault(instance.hasSeparator, false)) json.remove('hasSeparator'); if (isJsonDefault(instance.stacked, false)) json.remove('stacked'); + 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.wrapAlignment, MainAxisAlignmentC.start)) + json.remove('wrapAlignment'); return json; } @@ -375,6 +380,12 @@ ListViewProperties _$ListViewPropertiesFromJson(Map json) => ListViewProperties( separatorSpacing: (json['separatorSpacing'] as num?)?.toDouble() ?? 0, hasSeparator: json['hasSeparator'] as bool? ?? false, stacked: json['stacked'] as bool? ?? false, + wrap: json['wrap'] as bool? ?? false, + wrapSpacing: (json['wrapSpacing'] as num?)?.toDouble() ?? 0, + wrapRunSpacing: (json['wrapRunSpacing'] as num?)?.toDouble() ?? 0, + wrapAlignment: + $enumDecodeNullable(_$MainAxisAlignmentCEnumMap, json['wrapAlignment']) ?? + MainAxisAlignmentC.start, dividerProperties: json['dividerProperties'] == null ? null : DividerProperties.fromJson(json['dividerProperties'] as Map), @@ -390,9 +401,22 @@ Map _$ListViewPropertiesToJson(ListViewProperties instance) => 'hasSeparator': instance.hasSeparator, 'dividerProperties': instance.dividerProperties.toJson(), 'stacked': instance.stacked, + 'wrap': instance.wrap, + 'wrapSpacing': instance.wrapSpacing, + 'wrapRunSpacing': instance.wrapRunSpacing, + 'wrapAlignment': _$MainAxisAlignmentCEnumMap[instance.wrapAlignment]!, }; const _$ListItemSeparatorEnumMap = { ListItemSeparator.divider: 'divider', ListItemSeparator.space: 'space', }; + +const _$MainAxisAlignmentCEnumMap = { + MainAxisAlignmentC.start: 'start', + MainAxisAlignmentC.center: 'center', + MainAxisAlignmentC.end: 'end', + MainAxisAlignmentC.spaceBetween: 'spaceBetween', + MainAxisAlignmentC.spaceAround: 'spaceAround', + MainAxisAlignmentC.spaceEvenly: 'spaceEvenly', +}; diff --git a/test/edge_insets_json_test.dart b/test/edge_insets_json_test.dart new file mode 100644 index 00000000..7e6d9901 --- /dev/null +++ b/test/edge_insets_json_test.dart @@ -0,0 +1,31 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('padding reads spelled-out sides', () { + expect( + EdgeInsetsModel.fromJson({'left': 1, 'top': 2, 'right': 3, 'bottom': 4}), + EdgeInsetsModel.only(left: 1, top: 2, right: 3, bottom: 4), + ); + expect( + EdgeInsetsModel.fromJson({'horizontal': 16, 'vertical': 8}), + EdgeInsetsModel.symmetric(horizontal: 16, vertical: 8), + ); + expect( + EdgeInsetsModel.fromJson({'top': 12}), + EdgeInsetsModel.only(top: 12), + ); + }); + + test('the stored forms still read', () { + expect(EdgeInsetsModel.fromJson(8), EdgeInsetsModel.all(8)); + expect( + EdgeInsetsModel.fromJson([1, 2, 3, 4]), + EdgeInsetsModel.only(left: 1, top: 2, right: 3, bottom: 4), + ); + expect( + EdgeInsetsModel.fromJson({'l': 1, 't': 2, 'r': 3, 'b': 4}), + EdgeInsetsModel.only(left: 1, top: 2, right: 3, bottom: 4), + ); + }); +} diff --git a/test/list_wrap_test.dart b/test/list_wrap_test.dart new file mode 100644 index 00000000..f0813e9b --- /dev/null +++ b/test/list_wrap_test.dart @@ -0,0 +1,43 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('wrap settings survive JSON and are omitted at their defaults', () { + expect(ListViewProperties().toJson().keys, + isNot(anyOf(contains('wrap'), contains('wrapSpacing'), + contains('wrapRunSpacing'), contains('wrapAlignment')))); + + final ListViewProperties props = ListViewProperties( + wrap: true, + wrapSpacing: 8, + wrapRunSpacing: 6, + wrapAlignment: MainAxisAlignmentC.center, + ); + final Map json = props.toJson(); + expect(json['wrap'], isTrue); + expect(json['wrapSpacing'], 8); + expect(json['wrapRunSpacing'], 6); + expect(json['wrapAlignment'], 'center'); + + final ListViewProperties copy = ListViewProperties.fromJson(json); + expect(copy, props); + expect(copy.isWrapping, isTrue); + expect(props.copyWith(), props); + expect(props.copyWith(stacked: true).isWrapping, isFalse); + }); + + test('a wrapping list node round-trips', () { + final ListViewNode node = ListViewNode( + id: 'list', + name: 'Crates', + basicBoxLocal: NodeBox(0, 0, 300, 40), + properties: ListViewProperties(wrap: true, wrapSpacing: 4), + children: const [], + scrollDirection: AxisC.horizontal, + ); + final ListViewNode copy = ListViewNode.fromJson(node.toJson()); + expect(copy.properties.wrap, isTrue); + expect(copy.properties.wrapSpacing, 4); + expect(copy.scrollDirection, AxisC.horizontal); + }); +} diff --git a/test/style_property_variables_test.dart b/test/style_property_variables_test.dart new file mode 100644 index 00000000..5a1972dd --- /dev/null +++ b/test/style_property_variables_test.dart @@ -0,0 +1,55 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('A shape offers its effects, stroke weight and corner radius to set ' + 'value actions', () { + final RectangleNode node = RectangleNode( + id: 'chip', + name: 'Chip', + basicBoxLocal: NodeBox(0, 0, 80, 32), + strokeWeight: 2, + cornerRadius: const CornerRadius.all(RadiusModel.circular(6)), + effects: [ + Effect.dropShadow( + id: 'shadow', + visible: false, + radius: 0, + color: ColorRGBA.black, + offset: const Vec(2, 2), + spread: 0, + ), + ], + ); + final Map values = { + for (final ValueModel value in node.propertyVariables) + value.name: value.value, + }; + expect(values['effectsVisible'], isFalse); + expect(values['strokeWeight'], 2); + expect(values['cornerRadius'], 6); + }); + + test('A text offers its font weight as a number', () { + final TextNode node = TextNode( + id: 'text', + name: 'Text', + basicBoxLocal: NodeBox(0, 0, 80, 20), + characters: 'Oat', + textMixedProps: [ + StartEndProp( + start: 0, + end: 3, + fontName: const FontName( + family: 'Roboto', + weight: FontWeightNumeric.w700, + ), + ), + ], + ); + final ValueModel weight = node.propertyVariables.firstWhere( + (value) => value.name == 'fontWeight', + ); + expect(weight.value, 700); + }); +} From b558551aa7e160a3d0494a45666d2f135a9e821a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 03:41:43 +0000 Subject: [PATCH 3/9] Computed variables, looping and entering motion - VariableData.formula: a variable worked out from others. - MotionModel.loop (restart / reverse), enter and delayMs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013oMokUGNW6txknJs7xxTik --- lib/src/api/models/motion.dart | 56 +++++++++++++++++++ lib/src/api/models/motion.g.dart | 16 ++++++ lib/src/api/models/variables_model.dart | 35 +++++++++++- lib/src/api/models/variables_model.g.dart | 4 ++ test/computed_variable_test.dart | 66 +++++++++++++++++++++++ 5 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 test/computed_variable_test.dart diff --git a/lib/src/api/models/motion.dart b/lib/src/api/models/motion.dart index 69aa4704..b75d2a70 100644 --- a/lib/src/api/models/motion.dart +++ b/lib/src/api/models/motion.dart @@ -36,6 +36,25 @@ enum MotionCurve { }; } +/// Whether a [MotionModel] repeats on its own. +enum MotionLoop { + /// Moves only when its values change. + none, + + /// From rest to its values, then jumps back to rest and starts again. + restart, + + /// From rest to its values and back, over and over (a pulse). + reverse; + + /// Displayable name. + String get prettify => switch (this) { + MotionLoop.none => 'No loop', + MotionLoop.restart => 'Loop', + MotionLoop.reverse => 'Back and forth', + }; +} + /// How a node moves: a transform drawn on top of its layout (it never changes /// the space the node takes up), an animated transition whenever a value /// changes, and an optional continuous spin. @@ -100,6 +119,20 @@ class MotionModel with Equatable, SerializableMixin { /// How long the spin takes to reach a new speed, in milliseconds. final int spinRampMs; + /// Repeats the motion on its own, between the node at rest (no + /// transform) and these values, each way over [durationMs]: a skeleton's + /// pulse, a blinking cursor. + final MotionLoop loop; + + /// Animates from rest to these values when the node first appears, over + /// [durationMs], instead of starting there: a record sliding out of its + /// sleeve when the screen opens. + final bool enter; + + /// Waits this long, in milliseconds, before a transition, entrance or + /// loop starts, so motions can be staggered. + final int delayMs; + /// Creates a [MotionModel]. const MotionModel({ this.translateX = 0, @@ -116,6 +149,9 @@ class MotionModel with Equatable, SerializableMixin { this.curve = MotionCurve.easeInOut, this.spin = 0, this.spinRampMs = 800, + this.loop = MotionLoop.none, + this.enter = false, + this.delayMs = 0, }); /// No motion at all. @@ -176,6 +212,9 @@ class MotionModel with Equatable, SerializableMixin { MotionCurve? curve, double? spin, int? spinRampMs, + MotionLoop? loop, + bool? enter, + int? delayMs, }) => MotionModel( translateX: translateX ?? this.translateX, translateY: translateY ?? this.translateY, @@ -191,6 +230,20 @@ class MotionModel with Equatable, SerializableMixin { curve: curve ?? this.curve, spin: spin ?? this.spin, spinRampMs: spinRampMs ?? this.spinRampMs, + loop: loop ?? this.loop, + enter: enter ?? this.enter, + delayMs: delayMs ?? this.delayMs, + ); + + /// These settings with every value at rest: no transform at all. + MotionModel get atRest => copyWith( + translateX: 0, + translateY: 0, + scale: 1, + opacity: 1, + rotate: 0, + rotateX: 0, + rotateY: 0, ); /// Returns a copy with [property] set to [value]. @@ -222,6 +275,9 @@ class MotionModel with Equatable, SerializableMixin { curve, spin, spinRampMs, + loop, + enter, + delayMs, ]; /// Creates a [MotionModel] from JSON. diff --git a/lib/src/api/models/motion.g.dart b/lib/src/api/models/motion.g.dart index 4f36a89b..79eb0179 100644 --- a/lib/src/api/models/motion.g.dart +++ b/lib/src/api/models/motion.g.dart @@ -25,6 +25,9 @@ Map _$MotionModelOmitDefaults( json.remove('curve'); if (isJsonDefault(instance.spin, 0)) json.remove('spin'); if (isJsonDefault(instance.spinRampMs, 800)) json.remove('spinRampMs'); + if (isJsonDefault(instance.loop, MotionLoop.none)) json.remove('loop'); + if (isJsonDefault(instance.enter, false)) json.remove('enter'); + if (isJsonDefault(instance.delayMs, 0)) json.remove('delayMs'); return json; } @@ -49,6 +52,10 @@ MotionModel _$MotionModelFromJson(Map json) => MotionModel( MotionCurve.easeInOut, spin: (json['spin'] as num?)?.toDouble() ?? 0, spinRampMs: (json['spinRampMs'] as num?)?.toInt() ?? 800, + loop: + $enumDecodeNullable(_$MotionLoopEnumMap, json['loop']) ?? MotionLoop.none, + enter: json['enter'] as bool? ?? false, + delayMs: (json['delayMs'] as num?)?.toInt() ?? 0, ); Map _$MotionModelToJson(MotionModel instance) => @@ -67,6 +74,9 @@ Map _$MotionModelToJson(MotionModel instance) => 'curve': _$MotionCurveEnumMap[instance.curve]!, 'spin': instance.spin, 'spinRampMs': instance.spinRampMs, + 'loop': _$MotionLoopEnumMap[instance.loop]!, + 'enter': instance.enter, + 'delayMs': instance.delayMs, }; const _$MotionCurveEnumMap = { @@ -77,3 +87,9 @@ const _$MotionCurveEnumMap = { MotionCurve.easeOutBack: 'easeOutBack', MotionCurve.easeOutCubic: 'easeOutCubic', }; + +const _$MotionLoopEnumMap = { + MotionLoop.none: 'none', + MotionLoop.restart: 'restart', + MotionLoop.reverse: 'reverse', +}; diff --git a/lib/src/api/models/variables_model.dart b/lib/src/api/models/variables_model.dart index eb860565..e7e2c0b8 100644 --- a/lib/src/api/models/variables_model.dart +++ b/lib/src/api/models/variables_model.dart @@ -147,6 +147,25 @@ class VariableData /// metadata or any other information that is not part of the core variable. final Map extra; + /// The formula of a computed (derived) variable, or null for a normal + /// variable. + /// + /// A computed variable's value is not set by the user or by actions: it is + /// the result of this formula over other variables, e.g. + /// `${= filter(records, r => r.crate == crateId)}`. The formula is written + /// like a binding: `${= expression}`, a single variable path such as + /// `${records}`, or a bare expression (`length(records)`). It is + /// recomputed when a variable it reads changes. Set variable actions + /// don't change a computed variable. [value] is unused for it, and [type] + /// only describes the value the formula is expected to produce. + /// + /// Blank text counts as null, so `copyWith(formula: '')` makes the + /// variable a normal one again. + final String? formula; + + /// Whether this variable is computed from a [formula]. + bool get isComputed => formula != null; + /// Creates a new [VariableData]. VariableData({ required this.id, @@ -154,8 +173,10 @@ class VariableData Object? value = '', this.type = VariableType.text, Map? extra, + String? formula, }) : value = sanitizeValueForVariableType(value, type) ?? '', - extra = extra ?? {}; + extra = extra ?? {}, + formula = formula == null || formula.trim().isEmpty ? null : formula; /// Duplicate a [VariableData] with the given parameters. VariableData copyWith({ @@ -166,6 +187,7 @@ class VariableData bool? isUsed, Set? nodes, Map? extra, + String? formula, }) { final String? sanitizedValue = value == null ? null @@ -176,11 +198,12 @@ class VariableData value: sanitizedValue ?? this.value, type: type ?? this.type, extra: extra ?? this.extra, + formula: formula ?? this.formula, ); } @override - List get props => [id, name, value, type]; + List get props => [id, name, value, type, formula]; @override int compareTo(VariableData other) => name.compareTo(other.name); @@ -201,6 +224,8 @@ class VariableData name: name, value: value, type: type, + extra: extra, + formula: formula, ); /// Returns the value converted to the appropriate type according to [type]. @@ -277,6 +302,7 @@ class CanvasVariableData extends VariableData { super.type, super.value, super.extra, + super.formula, }); @override @@ -289,6 +315,7 @@ class CanvasVariableData extends VariableData { String? canvasId, Set? nodes, Map? extra, + String? formula, }) { final String? sanitizedValue = value == null ? null @@ -300,6 +327,7 @@ class CanvasVariableData extends VariableData { id: id ?? this.id, canvasId: canvasId ?? this.canvasId, extra: extra ?? this.extra, + formula: formula ?? this.formula, ); } @@ -335,6 +363,7 @@ class RuntimeVariableData extends VariableData { super.value = '', super.type = VariableType.text, super.extra, + super.formula, }) : super(id: id ?? generateId()); @override @@ -346,6 +375,7 @@ class RuntimeVariableData extends VariableData { bool? isUsed, Set? nodes, Map? extra, + String? formula, }) { final String? sanitizedValue = value == null ? null @@ -356,6 +386,7 @@ class RuntimeVariableData extends VariableData { value: sanitizedValue ?? this.value, type: type ?? this.type, extra: extra ?? this.extra, + formula: formula ?? this.formula, ); } } diff --git a/lib/src/api/models/variables_model.g.dart b/lib/src/api/models/variables_model.g.dart index 86c5e5f9..1d862617 100644 --- a/lib/src/api/models/variables_model.g.dart +++ b/lib/src/api/models/variables_model.g.dart @@ -41,6 +41,7 @@ VariableData _$VariableDataFromJson(Map json) => VariableData( ) ?? VariableType.text, extra: (json['extra'] as Map?)?.map((k, e) => MapEntry(k as String, e)), + formula: json['formula'] as String?, ); Map _$VariableDataToJson(VariableData instance) => @@ -50,6 +51,7 @@ Map _$VariableDataToJson(VariableData instance) => 'value': instance.value, 'type': _$VariableTypeEnumMap[instance.type]!, 'extra': instance.extra, + 'formula': ?instance.formula, }; const _$VariableTypeEnumMap = { @@ -77,6 +79,7 @@ CanvasVariableData _$CanvasVariableDataFromJson(Map json) => CanvasVariableData( VariableType.text, value: json['value'] ?? '', extra: (json['extra'] as Map?)?.map((k, e) => MapEntry(k as String, e)), + formula: json['formula'] as String?, ); Map _$CanvasVariableDataToJson(CanvasVariableData instance) => @@ -86,5 +89,6 @@ Map _$CanvasVariableDataToJson(CanvasVariableData instance) => 'value': instance.value, 'type': _$VariableTypeEnumMap[instance.type]!, 'extra': instance.extra, + 'formula': ?instance.formula, 'canvasId': instance.canvasId, }; diff --git a/test/computed_variable_test.dart b/test/computed_variable_test.dart new file mode 100644 index 00000000..8b3b8fe5 --- /dev/null +++ b/test/computed_variable_test.dart @@ -0,0 +1,66 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + const String formula = + r"${= sortBy(filter(records, r => crateId == 'all' || r.crate == crateId), sortBy)}"; + + group('computed variables', () { + test('a normal variable has no formula and writes none', () { + final variable = VariableData(id: 'a', name: 'a', value: 'x'); + expect(variable.formula, isNull); + expect(variable.isComputed, isFalse); + expect(variable.toJson().containsKey('formula'), isFalse); + }); + + test('formula round-trips through JSON', () { + final variable = VariableData( + id: 'v1', + name: 'visible', + type: VariableType.list, + formula: formula, + ); + final Map json = variable.toJson(); + expect(json, { + 'id': 'v1', + 'name': 'visible', + 'type': 'list', + 'formula': formula, + }); + final VariableData decoded = VariableData.fromJson(json); + expect(decoded.formula, formula); + expect(decoded.isComputed, isTrue); + expect(decoded, variable); + }); + + test('canvas variables keep the formula', () { + final canvasVariable = VariableData( + id: 'v1', + name: 'count', + type: VariableType.integer, + formula: r'${= length(records)}', + ).withCanvas('canvas1'); + expect(canvasVariable.formula, r'${= length(records)}'); + final decoded = CanvasVariableData.fromJson({ + ...canvasVariable.toJson(), + 'canvasId': 'canvas1', + }); + expect(decoded.formula, r'${= length(records)}'); + expect(decoded.copyWith(name: 'n').formula, r'${= length(records)}'); + }); + + test('a blank formula counts as none, so copyWith can clear it', () { + expect(VariableData(id: 'a', name: 'a', formula: ' ').formula, isNull); + final computed = VariableData(id: 'a', name: 'a', formula: 'x + 1'); + expect(computed.copyWith(formula: '').isComputed, isFalse); + expect(computed.copyWith(value: '3').formula, 'x + 1'); + }); + + test('the formula takes part in equality', () { + expect( + VariableData(id: 'a', name: 'a', formula: 'x'), + isNot(VariableData(id: 'a', name: 'a', formula: 'y')), + ); + }); + }); +} From a0553740f81bf3402e90ef5541847bc719b21afa Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 03:47:20 +0000 Subject: [PATCH 4/9] RowColumnNode.itemSpacing: a gap between children Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013oMokUGNW6txknJs7xxTik --- lib/src/api/nodes/row_column_node.dart | 25 ++++++++++ lib/src/api/nodes/row_column_node.g.dart | 5 +- test/nodes/row_column_item_spacing_test.dart | 52 ++++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 test/nodes/row_column_item_spacing_test.dart diff --git a/lib/src/api/nodes/row_column_node.dart b/lib/src/api/nodes/row_column_node.dart index 2094d2f7..76300452 100644 --- a/lib/src/api/nodes/row_column_node.dart +++ b/lib/src/api/nodes/row_column_node.dart @@ -1,5 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; +import '../field_access.dart'; import '../mixins.dart'; import '../models/models.dart'; @@ -24,6 +25,15 @@ class RowColumnNode extends ParentNode @override set scrollDirection(AxisC value) {} + /// The gap, in logical pixels, inserted between consecutive visible + /// children along the main axis. Equivalent to Flutter's + /// `Row(spacing: ...)` / `Column(spacing: ...)` and CSS `gap`. + /// + /// Only children that are actually laid out (visible) receive a gap; a + /// hidden child does not contribute an extra gap. Defaults to 0 and is + /// omitted from JSON at the default. + double itemSpacing; + /// Creates a [RowColumnNode] with the given data. RowColumnNode({ required super.id, @@ -63,6 +73,7 @@ class RowColumnNode extends ParentNode super.reactions, super.parentID, super.clipsContent = false, + this.itemSpacing = 0, // [CornerMixin] properties. CornerRadius cornerRadius = CornerRadius.zero, double cornerSmoothing = 0, @@ -101,6 +112,20 @@ class RowColumnNode extends ParentNode ); } + @override + FieldsMap generateFields() => { + ...super.generateFields(), + 'itemSpacing': NumFieldAccess( + 'Gap', + 'Space between consecutive children along the main axis.', + () => itemSpacing, + (value) => itemSpacing = value, + min: 0, + defaultValue: () => 0, + requiresLayout: true, + ), + }; + /// Creates a [RowColumnNode] from a JSON data. factory RowColumnNode.fromJson(Map json) => _$RowColumnNodeFromJson(json); diff --git a/lib/src/api/nodes/row_column_node.g.dart b/lib/src/api/nodes/row_column_node.g.dart index 20e5f40a..ff1782e0 100644 --- a/lib/src/api/nodes/row_column_node.g.dart +++ b/lib/src/api/nodes/row_column_node.g.dart @@ -10,6 +10,7 @@ Map _$RowColumnNodeOmitDefaults( RowColumnNode instance, Map json, ) { + if (isJsonDefault(instance.itemSpacing, 0)) json.remove('itemSpacing'); if (isJsonDefault(instance.visible, true)) json.remove('visible'); if (excludeConstraintsIf(instance)) json.remove('constraints'); if (isJsonDefault(instance.constraints, const BoxConstraintsModel())) @@ -205,6 +206,7 @@ RowColumnNode _$RowColumnNodeFromJson(Map json) => .toList() ?? const [], clipsContent: json['clipsContent'] as bool? ?? false, + itemSpacing: (json['itemSpacing'] as num?)?.toDouble() ?? 0, cornerRadius: json['cornerRadius'] == null ? CornerRadius.zero : CornerRadius.fromJson(json['cornerRadius']), @@ -280,6 +282,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 +300,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 +344,7 @@ Map _$RowColumnNodeToJson( 'useFlutterListView': instance.useFlutterListView, 'shouldAlwaysScroll': instance.shouldAlwaysScroll, 'type': instance.type, + 'itemSpacing': instance.itemSpacing, }; const _$RowColumnTypeEnumMap = { diff --git a/test/nodes/row_column_item_spacing_test.dart b/test/nodes/row_column_item_spacing_test.dart new file mode 100644 index 00000000..23c85ac6 --- /dev/null +++ b/test/nodes/row_column_item_spacing_test.dart @@ -0,0 +1,52 @@ +import 'dart:convert'; + +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +RowColumnNode _row({double itemSpacing = 0}) => RowColumnNode( + id: 'row', + name: 'Row', + basicBoxLocal: NodeBox(0, 0, 200, 50), + children: ['a', 'b'], + rowColumnType: RowColumnType.row, + itemSpacing: itemSpacing, +); + +Map _roundTripJson(RowColumnNode node) => + jsonDecode(jsonEncode(node.toJson())) as Map; + +void main() { + group('RowColumnNode.itemSpacing', () { + test('defaults to 0 and is omitted from JSON', () { + final node = _row(); + expect(node.itemSpacing, 0); + expect(node.toJson().containsKey('itemSpacing'), isFalse); + expect(RowColumnNode.fromJson(_roundTripJson(node)).itemSpacing, 0); + }); + + test('non-default value round-trips through JSON', () { + final node = _row(itemSpacing: 12.5); + final json = _roundTripJson(node); + expect(json['itemSpacing'], 12.5); + final decoded = RowColumnNode.fromJson(json); + expect(decoded.itemSpacing, 12.5); + expect(decoded.toJson(), node.toJson()); + }); + + test('accepts integer JSON values', () { + final json = _roundTripJson(_row(itemSpacing: 8)); + json['itemSpacing'] = 8; + expect(RowColumnNode.fromJson(json).itemSpacing, 8.0); + }); + + test('is exposed as a layout field', () { + final node = _row(itemSpacing: 4); + final field = node.fields['itemSpacing']; + expect(field, isNotNull); + expect(field!.getValue(), 4); + expect(field.requiresLayout, isTrue); + field.setValue(16); + expect(node.itemSpacing, 16); + }); + }); +} From 1fd7d2697eca0e3eda49d1ae3a4e5c7c5889c9aa Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 03:57:27 +0000 Subject: [PATCH 5/9] Double tap and hover triggers; vertical, inverted, stroked sliders with bindable range Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013oMokUGNW6txknJs7xxTik --- lib/src/api/models/trigger.dart | 27 +++++++- lib/src/api/models/trigger.g.dart | 3 + lib/src/api/nodes/button_node.dart | 2 + lib/src/api/nodes/frame_node.dart | 2 + lib/src/api/nodes/icon_node.dart | 2 + lib/src/api/nodes/rectangle_node.dart | 2 + lib/src/api/nodes/slider_node.dart | 66 +++++++++++++++++++- lib/src/api/nodes/slider_node.g.dart | 22 +++++++ lib/src/api/nodes/text_node.dart | 2 + test/double_tap_hover_slider_test.dart | 85 ++++++++++++++++++++++++++ 10 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 test/double_tap_hover_slider_test.dart diff --git a/lib/src/api/models/trigger.dart b/lib/src/api/models/trigger.dart index face0489..822203b4 100644 --- a/lib/src/api/models/trigger.dart +++ b/lib/src/api/models/trigger.dart @@ -68,7 +68,26 @@ enum TriggerType { swipeUp, /// Triggered when the user swipes the target down. - swipeDown; + swipeDown, + + /// Triggered when the user taps the target twice in quick succession. + /// A node with a double tap reaction delays its single taps ([click]) by + /// the double tap timeout (300ms), the way Flutter tells the two apart. + doubleTap, + + /// Triggered when a mouse pointer moves onto the target. Touch input never + /// fires it. + hoverEnter, + + /// Triggered when a mouse pointer leaves the target. Touch input never + /// fires it. + hoverExit; + + /// The hover triggers. + static const List hovers = [ + TriggerType.hoverEnter, + TriggerType.hoverExit, + ]; /// The swipe triggers. static const List swipes = [ @@ -119,6 +138,12 @@ enum TriggerType { return 'Swipe Up'; case TriggerType.swipeDown: return 'Swipe Down'; + case TriggerType.doubleTap: + return 'Double Tap'; + case TriggerType.hoverEnter: + return 'Hover Enter'; + case TriggerType.hoverExit: + return 'Hover Exit'; } } } diff --git a/lib/src/api/models/trigger.g.dart b/lib/src/api/models/trigger.g.dart index dcfb7065..34513224 100644 --- a/lib/src/api/models/trigger.g.dart +++ b/lib/src/api/models/trigger.g.dart @@ -55,4 +55,7 @@ const _$TriggerTypeEnumMap = { TriggerType.swipeRight: 'swipeRight', TriggerType.swipeUp: 'swipeUp', TriggerType.swipeDown: 'swipeDown', + TriggerType.doubleTap: 'doubleTap', + TriggerType.hoverEnter: 'hoverEnter', + TriggerType.hoverExit: 'hoverExit', }; diff --git a/lib/src/api/nodes/button_node.dart b/lib/src/api/nodes/button_node.dart index 76da81c8..1b565ed7 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.doubleTap, + ...TriggerType.hovers, ]; @override diff --git a/lib/src/api/nodes/frame_node.dart b/lib/src/api/nodes/frame_node.dart index a9031d16..79680397 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.doubleTap, + ...TriggerType.hovers, ...TriggerType.swipes, ]; diff --git a/lib/src/api/nodes/icon_node.dart b/lib/src/api/nodes/icon_node.dart index c217b72d..4f040a49 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.doubleTap, + ...TriggerType.hovers, ]; /// Creates an [IconNode] from a JSON data. diff --git a/lib/src/api/nodes/rectangle_node.dart b/lib/src/api/nodes/rectangle_node.dart index d5779f42..3b1bfe2d 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.doubleTap, + ...TriggerType.hovers, ...TriggerType.swipes, ]; diff --git a/lib/src/api/nodes/slider_node.dart b/lib/src/api/nodes/slider_node.dart index 89636208..2feb7ce0 100644 --- a/lib/src/api/nodes/slider_node.dart +++ b/lib/src/api/nodes/slider_node.dart @@ -114,12 +114,17 @@ class SliderNode extends SceneNode with CustomPropertiesMixin { }; @override - List get triggerTypes => [TriggerType.changed]; + List get triggerTypes => [ + TriggerType.changed, + TriggerType.doubleTap, + ]; @override late final List propertyVariables = [ ...super.propertyVariables, DoubleValue(name: 'value', value: value), + DoubleValue(name: 'min', value: properties.min), + DoubleValue(name: 'max', value: properties.max), ]; /// Creates a [SliderNode] instance from a JSON object. @@ -224,6 +229,22 @@ class SliderProperties extends CustomProperties with FieldsHolder { /// room for the thumb's overlay. Mirrors `Slider.padding`. EdgeInsetsModel? padding; + /// Direction the slider runs in. A vertical slider runs from [min] at the + /// bottom to [max] at the top (top to bottom when [inverted]); its node box + /// is its full hit area. + AxisC orientation; + + /// Whether the slider runs the other way: right to left when horizontal, + /// top to bottom when vertical. + bool inverted; + + /// Color of the stroke around the slider's thumb. No stroke when null or + /// when [thumbStrokeWidth] is 0. + ColorRGBA? thumbStrokeColor; + + /// Width of the stroke around the slider's thumb, drawn inside the thumb. + double thumbStrokeWidth; + /// Creates a [SliderProperties] instance with the given data. SliderProperties({ this.activeTrackColor = ColorRGBA.black, @@ -255,6 +276,10 @@ class SliderProperties extends CustomProperties with FieldsHolder { this.secondaryTrackValue, this.secondaryActiveTrackColor, this.padding, + this.orientation = AxisC.horizontal, + this.inverted = false, + this.thumbStrokeColor, + this.thumbStrokeWidth = 0, }); /// Duplicates this [SliderProperties] instance with the given data @@ -289,6 +314,10 @@ class SliderProperties extends CustomProperties with FieldsHolder { double? secondaryTrackValue, ColorRGBA? secondaryActiveTrackColor, EdgeInsetsModel? padding, + AxisC? orientation, + bool? inverted, + ColorRGBA? thumbStrokeColor, + double? thumbStrokeWidth, }) { return SliderProperties( activeTrackColor: activeTrackColor ?? this.activeTrackColor, @@ -326,6 +355,10 @@ class SliderProperties extends CustomProperties with FieldsHolder { secondaryActiveTrackColor: secondaryActiveTrackColor ?? this.secondaryActiveTrackColor, padding: padding ?? this.padding, + orientation: orientation ?? this.orientation, + inverted: inverted ?? this.inverted, + thumbStrokeColor: thumbStrokeColor ?? this.thumbStrokeColor, + thumbStrokeWidth: thumbStrokeWidth ?? this.thumbStrokeWidth, ); } @@ -528,6 +561,33 @@ class SliderProperties extends CustomProperties with FieldsHolder { () => secondaryActiveTrackColor, (value) => secondaryActiveTrackColor = value, ), + 'orientation': EnumFieldAccess( + 'Orientation', + 'Direction the slider runs in.', + () => orientation, + (AxisC value) => orientation = value, + defaultValue: () => AxisC.horizontal, + options: () => AxisC.values, + requiresLayout: true, + ), + 'inverted': BoolFieldAccess( + 'Inverted', + 'Whether the slider runs right to left, or top to bottom.', + () => inverted, + (bool value) => inverted = value, + ), + 'thumbStrokeColor': ColorFieldAccess( + 'Thumb Stroke Color', + 'Color of the stroke around the slider\'s thumb.', + () => thumbStrokeColor, + (value) => thumbStrokeColor = value, + ), + 'thumbStrokeWidth': NumFieldAccess( + 'Thumb Stroke Width', + 'Width of the stroke around the slider\'s thumb.', + () => thumbStrokeWidth, + (double value) => thumbStrokeWidth = value, + ), }; @override @@ -561,6 +621,10 @@ class SliderProperties extends CustomProperties with FieldsHolder { secondaryTrackValue, secondaryActiveTrackColor, padding, + orientation, + inverted, + thumbStrokeColor, + thumbStrokeWidth, ]; /// Creates a [SliderProperties] instance from a JSON object. diff --git a/lib/src/api/nodes/slider_node.g.dart b/lib/src/api/nodes/slider_node.g.dart index 0ea4e25d..ffed97e7 100644 --- a/lib/src/api/nodes/slider_node.g.dart +++ b/lib/src/api/nodes/slider_node.g.dart @@ -102,6 +102,11 @@ Map _$SliderPropertiesOmitDefaults( json.remove('overlayRadius'); if (isJsonDefault(instance.deriveOverlayColorFromThumb, true)) json.remove('deriveOverlayColorFromThumb'); + if (isJsonDefault(instance.orientation, AxisC.horizontal)) + json.remove('orientation'); + if (isJsonDefault(instance.inverted, false)) json.remove('inverted'); + if (isJsonDefault(instance.thumbStrokeWidth, 0)) + json.remove('thumbStrokeWidth'); return json; } @@ -334,6 +339,14 @@ SliderProperties _$SliderPropertiesFromJson(Map json) => SliderProperties( padding: json['padding'] == null ? null : EdgeInsetsModel.fromJson(json['padding']), + orientation: + $enumDecodeNullable(_$AxisCEnumMap, json['orientation']) ?? + AxisC.horizontal, + inverted: json['inverted'] as bool? ?? false, + thumbStrokeColor: json['thumbStrokeColor'] == null + ? null + : ColorRGBA.fromJson(json['thumbStrokeColor']), + thumbStrokeWidth: (json['thumbStrokeWidth'] as num?)?.toDouble() ?? 0, ); Map _$SliderPropertiesToJson( @@ -370,6 +383,10 @@ Map _$SliderPropertiesToJson( 'secondaryTrackValue': ?instance.secondaryTrackValue, 'secondaryActiveTrackColor': ?instance.secondaryActiveTrackColor?.toJson(), 'padding': ?instance.padding?.toJson(), + 'orientation': _$AxisCEnumMap[instance.orientation]!, + 'inverted': instance.inverted, + 'thumbStrokeColor': ?instance.thumbStrokeColor?.toJson(), + 'thumbStrokeWidth': instance.thumbStrokeWidth, }; const _$SliderTrackShapeEnumEnumMap = { @@ -392,3 +409,8 @@ const _$ShowValueIndicatorCEnumMap = { ShowValueIndicatorC.alwaysVisible: 'alwaysVisible', ShowValueIndicatorC.never: 'never', }; + +const _$AxisCEnumMap = { + AxisC.horizontal: 'horizontal', + AxisC.vertical: 'vertical', +}; diff --git a/lib/src/api/nodes/text_node.dart b/lib/src/api/nodes/text_node.dart index 520bdaa1..86571e6c 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.doubleTap, + ...TriggerType.hovers, ]; @override diff --git a/test/double_tap_hover_slider_test.dart b/test/double_tap_hover_slider_test.dart new file mode 100644 index 00000000..5d7611d6 --- /dev/null +++ b/test/double_tap_hover_slider_test.dart @@ -0,0 +1,85 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('Double tap and hover triggers survive JSON', () { + for (final type in [ + TriggerType.doubleTap, + TriggerType.hoverEnter, + TriggerType.hoverExit, + ]) { + final reaction = Reaction( + trigger: TriggerModel(type: type), + action: CallFunctionAction(name: 'f'), + ); + final json = reaction.toJson(); + expect(json['trigger'], {'type': type.name}); + expect(Reaction.fromJson(json).trigger.type, type); + } + expect(TriggerType.doubleTap.prettify, 'Double Tap'); + expect(TriggerType.hoverEnter.prettify, 'Hover Enter'); + }); + + test('Shapes, buttons, icons and sliders offer the new triggers', () { + final box = NodeBox(0, 0, 10, 10); + final frame = FrameNode( + id: 'f', + name: 'f', + basicBoxLocal: box, + children: const [], + ); + final rect = RectangleNode(id: 'r', name: 'r', basicBoxLocal: box); + final slider = SliderNode( + id: 's', + name: 's', + basicBoxLocal: box, + value: 0, + properties: SliderProperties(), + ); + for (final ReactionMixin node in [frame, rect]) { + expect( + node.triggerTypes, + containsAll([ + TriggerType.click, + TriggerType.doubleTap, + ...TriggerType.hovers, + ]), + ); + } + expect(slider.triggerTypes.first, TriggerType.changed); + expect(slider.triggerTypes, contains(TriggerType.doubleTap)); + expect( + slider.propertyVariables.map((v) => v.name), + containsAll(['value', 'min', 'max']), + ); + }); + + test('Slider orientation, inversion and thumb stroke survive JSON', () { + final props = SliderProperties( + min: -8, + max: 8, + orientation: AxisC.vertical, + inverted: true, + thumbStrokeColor: ColorRGBA.white, + thumbStrokeWidth: 2, + ); + final json = props.toJson(); + expect(json['orientation'], 'vertical'); + expect(json['inverted'], true); + expect(json['thumbStrokeWidth'], 2); + expect(json['thumbStrokeColor'], isNotNull); + final back = SliderProperties.fromJson(json); + expect(back, props); + + final defaults = SliderProperties().toJson(); + for (final key in [ + 'orientation', + 'inverted', + 'thumbStrokeWidth', + 'thumbStrokeColor', + ]) { + expect(defaults.containsKey(key), isFalse, reason: key); + } + expect(SliderProperties.fromJson({}).orientation, AxisC.horizontal); + }); +} From 02e04be61268805b59335f8ee47c44ae57eff763 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 16:47:05 +0000 Subject: [PATCH 6/9] Regenerate row_column_node.g.dart Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013oMokUGNW6txknJs7xxTik --- lib/src/api/nodes/row_column_node.g.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/api/nodes/row_column_node.g.dart b/lib/src/api/nodes/row_column_node.g.dart index ff1782e0..d20c0794 100644 --- a/lib/src/api/nodes/row_column_node.g.dart +++ b/lib/src/api/nodes/row_column_node.g.dart @@ -282,7 +282,6 @@ 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, @@ -300,6 +299,7 @@ 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(), From fa03985c00cc1f395b7d077d846132911daaaeaa Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:26:13 +0000 Subject: [PATCH 7/9] Format, document EdgeInsetsModel.fromJson, const test constructors Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013oMokUGNW6txknJs7xxTik --- lib/src/api/models/edge_insets.dart | 11 +++++++---- test/edge_insets_json_test.dart | 12 ++++++------ test/list_wrap_test.dart | 14 +++++++++++--- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/lib/src/api/models/edge_insets.dart b/lib/src/api/models/edge_insets.dart index bb9f1e69..a920d7ed 100644 --- a/lib/src/api/models/edge_insets.dart +++ b/lib/src/api/models/edge_insets.dart @@ -188,8 +188,6 @@ class EdgeInsetsModel with Equatable, DynamicSerializableMixin { @override List get props => [l, t, r, b]; - /// Factory constructor for creating a new [EdgeInsetsModel] instance - /// from JSON data. static const Set _longSideKeys = { 'left', 'top', @@ -202,6 +200,12 @@ class EdgeInsetsModel with Equatable, DynamicSerializableMixin { static double _side(Map json, String side, String axis) => ((json[side] ?? json[axis] ?? 0) as num).toDouble(); + /// Factory constructor for creating a new [EdgeInsetsModel] instance + /// from JSON data. + /// + /// Reads the stored forms (a number for all sides, `[horizontal, + /// vertical]`, `[l, t, r, b]`, `{l, t, r, b}`) and the spelled-out + /// `{left, top, right, bottom}` / `{horizontal, vertical}`. factory EdgeInsetsModel.fromJson(dynamic json) { return switch (json) { num all => EdgeInsetsModel.all(all.toDouble()), @@ -216,8 +220,7 @@ class EdgeInsetsModel with Equatable, DynamicSerializableMixin { bottom: bottom.toDouble(), ), // Spelled-out sides, as Flutter names them; missing sides are 0. - Map sides - when sides.keys.any(_longSideKeys.contains) => + Map sides when sides.keys.any(_longSideKeys.contains) => EdgeInsetsModel.only( left: _side(sides, 'left', 'horizontal'), top: _side(sides, 'top', 'vertical'), diff --git a/test/edge_insets_json_test.dart b/test/edge_insets_json_test.dart index 7e6d9901..0a9c96b8 100644 --- a/test/edge_insets_json_test.dart +++ b/test/edge_insets_json_test.dart @@ -5,27 +5,27 @@ void main() { test('padding reads spelled-out sides', () { expect( EdgeInsetsModel.fromJson({'left': 1, 'top': 2, 'right': 3, 'bottom': 4}), - EdgeInsetsModel.only(left: 1, top: 2, right: 3, bottom: 4), + const EdgeInsetsModel.only(left: 1, top: 2, right: 3, bottom: 4), ); expect( EdgeInsetsModel.fromJson({'horizontal': 16, 'vertical': 8}), - EdgeInsetsModel.symmetric(horizontal: 16, vertical: 8), + const EdgeInsetsModel.symmetric(horizontal: 16, vertical: 8), ); expect( EdgeInsetsModel.fromJson({'top': 12}), - EdgeInsetsModel.only(top: 12), + const EdgeInsetsModel.only(top: 12), ); }); test('the stored forms still read', () { - expect(EdgeInsetsModel.fromJson(8), EdgeInsetsModel.all(8)); + expect(EdgeInsetsModel.fromJson(8), const EdgeInsetsModel.all(8)); expect( EdgeInsetsModel.fromJson([1, 2, 3, 4]), - EdgeInsetsModel.only(left: 1, top: 2, right: 3, bottom: 4), + const EdgeInsetsModel.only(left: 1, top: 2, right: 3, bottom: 4), ); expect( EdgeInsetsModel.fromJson({'l': 1, 't': 2, 'r': 3, 'b': 4}), - EdgeInsetsModel.only(left: 1, top: 2, right: 3, bottom: 4), + const EdgeInsetsModel.only(left: 1, top: 2, right: 3, bottom: 4), ); }); } diff --git a/test/list_wrap_test.dart b/test/list_wrap_test.dart index f0813e9b..52b52629 100644 --- a/test/list_wrap_test.dart +++ b/test/list_wrap_test.dart @@ -3,9 +3,17 @@ import 'package:test/test.dart'; void main() { test('wrap settings survive JSON and are omitted at their defaults', () { - expect(ListViewProperties().toJson().keys, - isNot(anyOf(contains('wrap'), contains('wrapSpacing'), - contains('wrapRunSpacing'), contains('wrapAlignment')))); + expect( + ListViewProperties().toJson().keys, + isNot( + anyOf( + contains('wrap'), + contains('wrapSpacing'), + contains('wrapRunSpacing'), + contains('wrapAlignment'), + ), + ), + ); final ListViewProperties props = ListViewProperties( wrap: true, From fa4e5fe8ac48d3efa45ab8193f295d77e39e4a28 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:37:14 +0000 Subject: [PATCH 8/9] Motion entrances from values with replay, rectangular slider thumbs, formulas with braces in variable syntax - MotionModel.enterFrom and a bindable replayKey. - SliderProperties.thumbShape (circle, roundedRect) with size and corners. - variableSyntaxIdentifierPattern matches a ${= ...} formula whole, object literals included. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013oMokUGNW6txknJs7xxTik --- lib/src/api/mixins.dart | 16 +++ lib/src/api/models/motion.dart | 50 ++++++++- lib/src/api/models/motion.g.dart | 6 ++ lib/src/api/nodes/slider_node.dart | 67 +++++++++++- lib/src/api/nodes/slider_node.g.dart | 30 ++++++ lib/src/api/regexes.dart | 19 +++- test/slider_thumb_motion_entrance_test.dart | 108 ++++++++++++++++++++ 7 files changed, 290 insertions(+), 6 deletions(-) create mode 100644 test/slider_thumb_motion_entrance_test.dart diff --git a/lib/src/api/mixins.dart b/lib/src/api/mixins.dart index 513155f4..d1570d0b 100644 --- a/lib/src/api/mixins.dart +++ b/lib/src/api/mixins.dart @@ -562,6 +562,22 @@ enum SliderTrackShapeEnum { } } +/// Represents the shape of a slider's thumb. +enum SliderThumbShapeEnum { + /// A circle of the slider's thumb radius. + circle, + + /// A rectangle of the slider's thumb width and height with rounded + /// corners, like a fader cap. + roundedRect; + + /// Display-able string representation of the enum. + String get prettify => switch (this) { + SliderThumbShapeEnum.circle => 'Circle', + SliderThumbShapeEnum.roundedRect => 'Rectangle', + }; +} + /// When a slider shows its value indicator. Mirrors Flutter's /// `ShowValueIndicator`. enum ShowValueIndicatorC { diff --git a/lib/src/api/models/motion.dart b/lib/src/api/models/motion.dart index b75d2a70..e0fc3849 100644 --- a/lib/src/api/models/motion.dart +++ b/lib/src/api/models/motion.dart @@ -124,11 +124,23 @@ class MotionModel with Equatable, SerializableMixin { /// pulse, a blinking cursor. final MotionLoop loop; - /// Animates from rest to these values when the node first appears, over - /// [durationMs], instead of starting there: a record sliding out of its - /// sleeve when the screen opens. + /// Animates from rest (or from [enterFrom]) to these values when the node + /// first appears, over [durationMs], instead of starting there: a record + /// sliding out of its sleeve when the screen opens. final bool enter; + /// Where the entrance starts instead of rest: its offset, scale, opacity, + /// rotation and tilts (its other settings are ignored). Setting it plays + /// an entrance even when [enter] is false: a record fading and settling in + /// from `{"scale": 1.15, "opacity": 0}`. + final MotionModel? enterFrom; + + /// Plays the entrance again, from [enterFrom] or rest, whenever this + /// changes. Meant to be bound through `motion.replayKey` in + /// [BaseNode.variables] to any value, for example `${playingId}`, so + /// loading another record replays its entrance. + final String? replayKey; + /// Waits this long, in milliseconds, before a transition, entrance or /// loop starts, so motions can be staggered. final int delayMs; @@ -151,6 +163,8 @@ class MotionModel with Equatable, SerializableMixin { this.spinRampMs = 800, this.loop = MotionLoop.none, this.enter = false, + this.enterFrom, + this.replayKey, this.delayMs = 0, }); @@ -170,6 +184,10 @@ class MotionModel with Equatable, SerializableMixin { 'spin', ]; + /// The name of the binding that replays the entrance when its value + /// changes; see [replayKey]. + static const String replayKeyProperty = 'replayKey'; + /// The key in [BaseNode.variables] that binds [property]. static String variableKey(String property) => 'motion.$property'; @@ -183,6 +201,24 @@ class MotionModel with Equatable, SerializableMixin { rotateX != 0 || rotateY != 0; + /// Whether the node animates in when it first appears. + bool get hasEntrance => enter || enterFrom != null; + + /// Where the entrance starts: these settings with the transform of + /// [enterFrom], or at rest when there is none. + MotionModel get entranceStart => switch (enterFrom) { + null => atRest, + MotionModel from => copyWith( + translateX: from.translateX, + translateY: from.translateY, + scale: from.scale, + opacity: from.opacity, + rotate: from.rotate, + rotateX: from.rotateX, + rotateY: from.rotateY, + ), + }; + /// The value of [property] in this model. double valueOf(String property) => switch (property) { 'translateX' => translateX, @@ -214,6 +250,10 @@ class MotionModel with Equatable, SerializableMixin { int? spinRampMs, MotionLoop? loop, bool? enter, + MotionModel? enterFrom, + bool clearEnterFrom = false, + String? replayKey, + bool clearReplayKey = false, int? delayMs, }) => MotionModel( translateX: translateX ?? this.translateX, @@ -232,6 +272,8 @@ class MotionModel with Equatable, SerializableMixin { spinRampMs: spinRampMs ?? this.spinRampMs, loop: loop ?? this.loop, enter: enter ?? this.enter, + enterFrom: clearEnterFrom ? null : enterFrom ?? this.enterFrom, + replayKey: clearReplayKey ? null : replayKey ?? this.replayKey, delayMs: delayMs ?? this.delayMs, ); @@ -277,6 +319,8 @@ class MotionModel with Equatable, SerializableMixin { spinRampMs, loop, enter, + enterFrom, + replayKey, delayMs, ]; diff --git a/lib/src/api/models/motion.g.dart b/lib/src/api/models/motion.g.dart index 79eb0179..83b196ea 100644 --- a/lib/src/api/models/motion.g.dart +++ b/lib/src/api/models/motion.g.dart @@ -55,6 +55,10 @@ MotionModel _$MotionModelFromJson(Map json) => MotionModel( loop: $enumDecodeNullable(_$MotionLoopEnumMap, json['loop']) ?? MotionLoop.none, enter: json['enter'] as bool? ?? false, + enterFrom: json['enterFrom'] == null + ? null + : MotionModel.fromJson(json['enterFrom'] as Map), + replayKey: json['replayKey'] as String?, delayMs: (json['delayMs'] as num?)?.toInt() ?? 0, ); @@ -76,6 +80,8 @@ Map _$MotionModelToJson(MotionModel instance) => 'spinRampMs': instance.spinRampMs, 'loop': _$MotionLoopEnumMap[instance.loop]!, 'enter': instance.enter, + 'enterFrom': ?instance.enterFrom?.toJson(), + 'replayKey': ?instance.replayKey, 'delayMs': instance.delayMs, }; diff --git a/lib/src/api/nodes/slider_node.dart b/lib/src/api/nodes/slider_node.dart index 2feb7ce0..265c785a 100644 --- a/lib/src/api/nodes/slider_node.dart +++ b/lib/src/api/nodes/slider_node.dart @@ -22,6 +22,12 @@ const double kSliderDefaultTickMarkRadius = 1; /// Default radius of slider's thumb. const double kSliderDefaultThumbRadius = 10; +/// Default width and height of a slider's rectangular thumb. +const double kSliderDefaultThumbSize = 20; + +/// Default corner radius of a slider's rectangular thumb. +const double kSliderDefaultThumbCornerRadius = 4; + /// Default radius of slider's overlay. const double kSliderDefaultOverlayRadius = 24; @@ -191,9 +197,26 @@ class SliderProperties extends CustomProperties with FieldsHolder { /// Whether to allow non-whole numbers as min or max values of slider. bool allowFractionalPoints; - /// Radius of slider's thumb. + /// Radius of slider's thumb when [thumbShape] is + /// [SliderThumbShapeEnum.circle]. double thumbRadius; + /// Shape of slider's thumb: a circle of [thumbRadius], or a rounded + /// rectangle of [thumbWidth] by [thumbHeight] with [thumbCornerRadius] + /// corners. + SliderThumbShapeEnum thumbShape; + + /// Width of a rectangular thumb as drawn on screen, across a vertical + /// slider's track and along a horizontal one's. + double thumbWidth; + + /// Height of a rectangular thumb as drawn on screen, along a vertical + /// slider's track and across a horizontal one's. + double thumbHeight; + + /// Corner radius of a rectangular thumb. + double thumbCornerRadius; + /// Whether to show slider's thumb. bool showThumb; @@ -266,6 +289,10 @@ class SliderProperties extends CustomProperties with FieldsHolder { this.valueIndicatorTextColor = ColorRGBA.white, this.valueIndicatorFontSize = 14, this.thumbRadius = kSliderDefaultThumbRadius, + this.thumbShape = SliderThumbShapeEnum.circle, + this.thumbWidth = kSliderDefaultThumbSize, + this.thumbHeight = kSliderDefaultThumbSize, + this.thumbCornerRadius = kSliderDefaultThumbCornerRadius, this.showThumb = true, this.trackShape = SliderTrackShapeEnum.roundedRectangle, this.tickMarkRadius = kSliderDefaultTickMarkRadius, @@ -304,6 +331,10 @@ class SliderProperties extends CustomProperties with FieldsHolder { ColorRGBA? valueIndicatorTextColor, double? valueIndicatorFontSize, double? thumbRadius, + SliderThumbShapeEnum? thumbShape, + double? thumbWidth, + double? thumbHeight, + double? thumbCornerRadius, bool? showThumb, SliderTrackShapeEnum? trackShape, double? tickMarkRadius, @@ -343,6 +374,10 @@ class SliderProperties extends CustomProperties with FieldsHolder { valueIndicatorFontSize: valueIndicatorFontSize ?? this.valueIndicatorFontSize, thumbRadius: thumbRadius ?? this.thumbRadius, + thumbShape: thumbShape ?? this.thumbShape, + thumbWidth: thumbWidth ?? this.thumbWidth, + thumbHeight: thumbHeight ?? this.thumbHeight, + thumbCornerRadius: thumbCornerRadius ?? this.thumbCornerRadius, showThumb: showThumb ?? this.showThumb, trackShape: trackShape ?? this.trackShape, tickMarkRadius: tickMarkRadius ?? this.tickMarkRadius, @@ -588,6 +623,32 @@ class SliderProperties extends CustomProperties with FieldsHolder { () => thumbStrokeWidth, (double value) => thumbStrokeWidth = value, ), + 'thumbShape': EnumFieldAccess( + 'Thumb Shape', + 'Shape of slider\'s thumb: a circle or a rounded rectangle.', + () => thumbShape, + (SliderThumbShapeEnum value) => thumbShape = value, + defaultValue: () => SliderThumbShapeEnum.circle, + options: () => SliderThumbShapeEnum.values, + ), + 'thumbWidth': NumFieldAccess( + 'Thumb Width', + 'Width of slider\'s rectangular thumb.', + () => thumbWidth, + (double value) => thumbWidth = value, + ), + 'thumbHeight': NumFieldAccess( + 'Thumb Height', + 'Height of slider\'s rectangular thumb.', + () => thumbHeight, + (double value) => thumbHeight = value, + ), + 'thumbCornerRadius': NumFieldAccess( + 'Thumb Corner Radius', + 'Corner radius of slider\'s rectangular thumb.', + () => thumbCornerRadius, + (double value) => thumbCornerRadius = value, + ), }; @override @@ -611,6 +672,10 @@ class SliderProperties extends CustomProperties with FieldsHolder { valueIndicatorTextColor, valueIndicatorFontSize, thumbRadius, + thumbShape, + thumbWidth, + thumbHeight, + thumbCornerRadius, showThumb, trackShape, tickMarkRadius, diff --git a/lib/src/api/nodes/slider_node.g.dart b/lib/src/api/nodes/slider_node.g.dart index ffed97e7..73b59d98 100644 --- a/lib/src/api/nodes/slider_node.g.dart +++ b/lib/src/api/nodes/slider_node.g.dart @@ -88,6 +88,17 @@ Map _$SliderPropertiesOmitDefaults( json.remove('allowFractionalPoints'); if (isJsonDefault(instance.thumbRadius, kSliderDefaultThumbRadius)) json.remove('thumbRadius'); + if (isJsonDefault(instance.thumbShape, SliderThumbShapeEnum.circle)) + json.remove('thumbShape'); + if (isJsonDefault(instance.thumbWidth, kSliderDefaultThumbSize)) + json.remove('thumbWidth'); + if (isJsonDefault(instance.thumbHeight, kSliderDefaultThumbSize)) + json.remove('thumbHeight'); + if (isJsonDefault( + instance.thumbCornerRadius, + kSliderDefaultThumbCornerRadius, + )) + json.remove('thumbCornerRadius'); if (isJsonDefault(instance.showThumb, true)) json.remove('showThumb'); if (isJsonDefault(instance.trackShape, SliderTrackShapeEnum.roundedRectangle)) json.remove('trackShape'); @@ -310,6 +321,16 @@ SliderProperties _$SliderPropertiesFromJson(Map json) => SliderProperties( (json['valueIndicatorFontSize'] as num?)?.toDouble() ?? 14, thumbRadius: (json['thumbRadius'] as num?)?.toDouble() ?? kSliderDefaultThumbRadius, + thumbShape: + $enumDecodeNullable(_$SliderThumbShapeEnumEnumMap, json['thumbShape']) ?? + SliderThumbShapeEnum.circle, + thumbWidth: + (json['thumbWidth'] as num?)?.toDouble() ?? kSliderDefaultThumbSize, + thumbHeight: + (json['thumbHeight'] as num?)?.toDouble() ?? kSliderDefaultThumbSize, + thumbCornerRadius: + (json['thumbCornerRadius'] as num?)?.toDouble() ?? + kSliderDefaultThumbCornerRadius, showThumb: json['showThumb'] as bool? ?? true, trackShape: $enumDecodeNullable(_$SliderTrackShapeEnumEnumMap, json['trackShape']) ?? @@ -371,6 +392,10 @@ Map _$SliderPropertiesToJson( 'valueIndicatorFontSize': instance.valueIndicatorFontSize, 'allowFractionalPoints': instance.allowFractionalPoints, 'thumbRadius': instance.thumbRadius, + 'thumbShape': _$SliderThumbShapeEnumEnumMap[instance.thumbShape]!, + 'thumbWidth': instance.thumbWidth, + 'thumbHeight': instance.thumbHeight, + 'thumbCornerRadius': instance.thumbCornerRadius, 'showThumb': instance.showThumb, 'trackShape': _$SliderTrackShapeEnumEnumMap[instance.trackShape]!, 'tickMarkRadius': instance.tickMarkRadius, @@ -389,6 +414,11 @@ Map _$SliderPropertiesToJson( 'thumbStrokeWidth': instance.thumbStrokeWidth, }; +const _$SliderThumbShapeEnumEnumMap = { + SliderThumbShapeEnum.circle: 'circle', + SliderThumbShapeEnum.roundedRect: 'roundedRect', +}; + const _$SliderTrackShapeEnumEnumMap = { SliderTrackShapeEnum.rectangle: 'rectangle', SliderTrackShapeEnum.roundedRectangle: 'roundedRectangle', diff --git a/lib/src/api/regexes.dart b/lib/src/api/regexes.dart index 5e35f2d2..66a07b4d 100644 --- a/lib/src/api/regexes.dart +++ b/lib/src/api/regexes.dart @@ -128,9 +128,24 @@ const String variablePathComposingPattern = /// Regex for [variablePathComposingPattern]. final RegExp variablePathComposingRegex = RegExp(variablePathComposingPattern); +const String _formulaAtom = r'''[^{}'"]|'[^']*'|"[^"]*"'''; +const String _formulaBody0 = '(?:$_formulaAtom)*'; +const String _formulaBody1 = '(?:$_formulaAtom|\\{$_formulaBody0\\})*'; +const String _formulaBody2 = '(?:$_formulaAtom|\\{$_formulaBody1\\})*'; + +/// The expression of a `${= ...}` formula: text and quoted strings, with +/// braces only in balanced pairs nested up to three deep, such as the object +/// literal in `${= merge(r, {plays: r.plays + 1})}`. Braces inside quoted +/// strings don't count. +const String formulaExpressionPattern = + '(?:$_formulaAtom|\\{$_formulaBody2\\})*'; + /// A regex that matches any variable path identifier in a string that is -/// wrapped in ${} curly braces. -const String variableSyntaxIdentifierPattern = r'(?[^}]+)\}'; +/// wrapped in ${} curly braces. A formula (`${= ...}`) matches whole, braces +/// of its object literals included, so `${= merge(r, {a: 1})}` is one match +/// rather than ending at the first `}`. +const String variableSyntaxIdentifierPattern = + '(?=$formulaExpressionPattern|[^}]+)\\}'; /// A regex that can match empty ${} syntax with partial match for curly braces. const String emptyVariableSyntaxIdentifierPattern = r'^(? json = properties.toJson(); + for (final String key in [ + 'thumbShape', + 'thumbWidth', + 'thumbHeight', + 'thumbCornerRadius', + ]) { + expect(json.containsKey(key), isFalse, reason: key); + } + }); + + test('a rounded rectangle thumb round-trips', () { + final SliderProperties properties = SliderProperties.fromJson({ + 'thumbShape': 'roundedRect', + 'thumbWidth': 44, + 'thumbHeight': 14, + 'thumbCornerRadius': 2, + }); + expect(properties.thumbShape, SliderThumbShapeEnum.roundedRect); + expect(properties.toJson(), { + 'thumbShape': 'roundedRect', + 'thumbWidth': 44.0, + 'thumbHeight': 14.0, + 'thumbCornerRadius': 2.0, + }); + expect(SliderProperties.fromJson(properties.toJson()), properties); + expect( + properties.copyWith(thumbShape: SliderThumbShapeEnum.circle).thumbWidth, + 44, + ); + }); + + test('padding 0 is kept; no padding stays null', () { + expect(SliderProperties.fromJson({}).padding, isNull); + final SliderProperties zero = SliderProperties.fromJson({'padding': 0}); + expect(zero.padding, EdgeInsetsModel.zero); + expect(zero.toJson(), {'padding': 0}); + }); + }); + + group('Motion entrance', () { + test('enterFrom implies an entrance that starts from its values', () { + const MotionModel motion = MotionModel( + opacity: 0.8, + durationMs: 300, + enterFrom: MotionModel(scale: 1.15, opacity: 0), + ); + expect(motion.hasEntrance, isTrue); + expect(MotionModel.none.hasEntrance, isFalse); + expect(const MotionModel(enter: true).hasEntrance, isTrue); + final MotionModel start = motion.entranceStart; + expect(start.scale, 1.15); + expect(start.opacity, 0); + expect(start.durationMs, 300); + expect(const MotionModel(scale: 2).entranceStart.scale, 1); + }); + + test('enterFrom and replayKey round-trip and clear', () { + const MotionModel motion = MotionModel( + enterFrom: MotionModel(translateY: 12), + replayKey: 'side-a', + ); + expect(motion.toJson(), { + 'enterFrom': {'translateY': 12.0}, + 'replayKey': 'side-a', + }); + expect(MotionModel.fromJson(motion.toJson()), motion); + expect( + motion.copyWith(clearEnterFrom: true, clearReplayKey: true), + MotionModel.none, + ); + expect( + MotionModel.variableKey(MotionModel.replayKeyProperty), + 'motion.replayKey', + ); + }); + }); + + test('variable syntax matches keep a formula with object literals whole', () { + const String formula = + r'${= map(records, r => r.id == x ? merge(r, {plays: r.plays + 1}) : r)}'; + expect( + [ + for (final m in variableSyntaxIdentifierRegex.allMatches( + 'a $formula b \${c.d} \${= {k: {v: 1}}.k.v}', + )) + m[0], + ], + [formula, r'${c.d}', r'${= {k: {v: 1}}.k.v}'], + ); + // Renaming a variable leaves formulas as they are and renames paths. + expect( + 'x $formula \${records}'.replaceVariableName('records', 'crate'), + 'x $formula \${crate}', + ); + }); +} From e8563c2657a9735b9f7cefe4c3aebe4381f3457d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:41:04 +0000 Subject: [PATCH 9/9] codelessly_api 6.2.0 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013oMokUGNW6txknJs7xxTik --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++++ pubspec.yaml | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cb1e325..9912638b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,38 @@ +## 6.2.0 + +### Motion + +- `MotionModel` on every node (`BaseNode.motion`): offset, scale, opacity, + rotation, 3D tilt with perspective and origin, an animated transition + (duration and curve), continuous spin, `loop` (restart or back and forth), + `enter`, `enterFrom`, `delayMs` and a bindable `replayKey`. Values bind as + `motion.` variables. + +### Triggers + +- `timer` (`TriggerModel.intervalMs`) and `keyPress` (`TriggerModel.key`) on + canvases; `swipeLeft/Right/Up/Down`, `doubleTap`, `hoverEnter` and + `hoverExit` on frames, rectangles, buttons, icons and text. + +### Layout + +- `RowColumnNode.itemSpacing`: a gap between children. +- `ListViewProperties.stacked` (items drawn on top of each other) and `wrap` + with `wrapSpacing`, `wrapRunSpacing` and `wrapAlignment`. +- Sliders: `orientation` (vertical), `inverted`, thumb stroke, and a + `roundedRect` thumb shape with width, height and corners. + +### Variables and JSON + +- Computed variables: `VariableData.formula`. +- Bindable `effectsVisible`, `strokeWeight`, `cornerRadius`, `fontWeight` + and a slider's `min`/`max` as property variables. +- `variableSyntaxIdentifierPattern` matches a `${= ...}` formula whole, + object literals included. +- `EdgeInsetsModel.fromJson` reads `{left, top, right, bottom}` and + `{horizontal, vertical}`; placeholders and lists read JSON without their + editor-only keys. + ## 6.1.0 ### States and conditions diff --git a/pubspec.yaml b/pubspec.yaml index 93e60aa4..4efa4b73 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: codelessly_api description: Flutter's layouts and widgets represented as models and JSON data. This API is used by the Codelessly Editor and CloudUI SDK to render Flutter Widgets. -version: 6.1.0 +version: 6.2.0 homepage: https://codelessly.com/ repository: https://github.com/Codelessly/CodelesslyAPI