Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
086dd91
Render gauge and bar chart nodes; run interval reactions
claude Oct 22, 2026
025cd12
Formulas: object() records and clamp, sin, cos, sqrt, pow, pad
claude Oct 22, 2026
e28a2a9
Show dialogs as bottom sheets, adaptively, or anchored to the trigger
claude Oct 22, 2026
e69e40a
Evaluate computed variables and rebuild on what their formulas read
claude Oct 22, 2026
0b68c52
FormulaEvaluator.parses
claude Oct 22, 2026
729e4da
Slider: fader thumb, end fill, notches, reading marker, selectable range
claude Oct 22, 2026
13585ef
Bindable text color; alpha() formula function
claude Oct 22, 2026
848843c
Place canvas confirm dialogs like Show Dialog
claude Oct 22, 2026
de6558f
Formulas: take, skip, keys
claude Oct 22, 2026
246720a
Toast: alignment, side margin, max width, corner radius, action color…
claude Oct 22, 2026
74c6cd0
Gauge and bar chart widgets take the caller's scoped values
claude Oct 23, 2026
289da28
Rows that hug their cross axis and stretch give hugging children the …
claude Oct 23, 2026
7f8502d
Animate bound opacity changes of nodes with a transition
claude Oct 23, 2026
ba5a614
Formulas join lists (concat, list + list); icons can be bound to an i…
claude Oct 23, 2026
72099f4
Formula docs: concat example parses
claude Oct 23, 2026
ddd3288
Computed variables coerce Choice variables to their option id; tidy t…
claude Oct 23, 2026
bb5267b
codelessly_sdk 8.3.0
claude Oct 23, 2026
c5c5de3
Format and lint for CI: dart format, unused import, single quotes
claude Oct 23, 2026
71c113d
Apply a bound cornerRadius at runtime
claude Oct 24, 2026
6aea6eb
ScopedValues.preview takes sample route params
claude Oct 24, 2026
9d26577
An icon bound to a name loads its own version's SVG
claude Oct 24, 2026
040d203
PassiveTextFieldWidget takes the scoped values its bound text reads
claude Oct 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## Unreleased
## 8.3.0

### Layout

Expand Down Expand Up @@ -31,6 +31,39 @@
- A text field's `errorText` template that comes out empty is no error.
- List items on the design canvas show their own item's text.

### Widgets

- Gauges (`PassiveGaugeWidget`, `GaugePainter`) and bar charts
(`PassiveBarChartWidget`) render the new `gauge` and `barChart` nodes; both
take `scopedValues` so an editor can preview their bindings.
- Industrial slider: rectangle fader thumb with grip, fill from the thumb to
the end, notch tick marks, a value marker, a bindable active track color,
a selectable range and the `changeEnd` trigger.
- Text nodes take a bound `textColor`; icons take an `icon` binding naming a
Material icon (`iconWithName`).
- Nodes with a `transition` animate runtime opacity changes.
- `ScopedValues.preview` takes `routeParams`, so a design canvas can show
`${route.…}` with sample params.
- An icon bound to a name loads the SVG of that icon's version
(`materialIconVersions`), so icons added after version 1 (`air`) draw.
- `PassiveTextFieldWidget.scopedValues`: the values a field's bound and
initial text read, so an editor can show variable defaults.
- Nodes with corners take a `cornerRadius` binding: a number or
`[top-left, top-right, bottom-right, bottom-left]`.
- A row or column that hugs its cross axis and stretches its children gives
hugging children the thickest child's size, as the editor lays it out.

### Actions and variables

- Interval reactions run on a timer per period while a canvas shows.
- Dialogs open centered, as a bottom sheet, adaptively or anchored under the
trigger; canvas confirms take the same placement.
- Toasts: alignment, side margin, max width, corner radius, action color,
close button and font size.
- Computed variables also come from `VariableData.formula`, coerced to the
variable's type; `setVariable` leaves computed variables alone.
- Formulas: `pad`, `alpha`, `concat` and list `+`; `FormulaEvaluator.parses`.

## 8.2.0

### States
Expand Down
1 change: 1 addition & 0 deletions lib/src/functions/app_confirm_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class AppConfirmDialog {
params: action.params,
barrierDismissible: action.barrierDismissible,
barrierColor: action.barrierColor?.toFlutterColor(),
placement: action.placement,
);
final bool confirmed = ConfirmAction.isConfirmResult(result);
return (
Expand Down
53 changes: 42 additions & 11 deletions lib/src/functions/app_toast.dart
Original file line number Diff line number Diff line change
Expand Up @@ -178,23 +178,31 @@ class _ToastViewState extends State<_ToastView> {
child: retrieveIconWidget(action.icon!, 18) ?? const SizedBox(),
);
final bool hasButton = widget.onAction != null;
final bool hasClose = action.showCloseButton;
final bool stretch = action.alignment == ToastAlignment.stretch;
final bool top = action.position == ToastPosition.top;
final EdgeInsets safe = MediaQuery.paddingOf(context);
final TextStyle messageStyle = theme.bodyStyle.copyWith(
color: textColor,
fontSize: 14,
fontSize: action.fontSize,
height: 1.35,
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
);

Widget toast = Container(
constraints: const BoxConstraints(minHeight: 44, maxWidth: 560),
padding: EdgeInsets.fromLTRB(18, 10, hasButton ? 8 : 18, 10),
constraints: BoxConstraints(minHeight: 44, maxWidth: action.maxWidth),
padding: EdgeInsets.fromLTRB(
18,
hasButton || hasClose ? 6 : 10,
hasButton || hasClose ? 8 : 18,
hasButton || hasClose ? 6 : 10,
),
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(
hasButton ? theme.buttonRadius.clamp(8, 999) : 999,
action.cornerRadius ??
(hasButton ? theme.buttonRadius.clamp(8, 999) : 999),
),
boxShadow: const [
BoxShadow(
Expand All @@ -205,17 +213,21 @@ class _ToastViewState extends State<_ToastView> {
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisSize: stretch ? MainAxisSize.max : MainAxisSize.min,
children: [
if (icon != null) ...[icon, const SizedBox(width: 8)],
Flexible(child: Text(widget.message, style: messageStyle)),
if (stretch)
Expanded(child: Text(widget.message, style: messageStyle))
else
Flexible(child: Text(widget.message, style: messageStyle)),
if (hasButton) ...[
const SizedBox(width: 8),
TextButton(
key: const ValueKey('codelessly-toast-action'),
onPressed: _onActionPressed,
style: TextButton.styleFrom(
foregroundColor: textColor,
foregroundColor:
action.actionColor?.toFlutterColor() ?? textColor,
minimumSize: const Size(48, 36),
padding: const EdgeInsets.symmetric(horizontal: 12),
textStyle: theme.buttonStyle.copyWith(
Expand All @@ -229,6 +241,16 @@ class _ToastViewState extends State<_ToastView> {
child: Text(widget.actionLabel),
),
],
if (hasClose)
IconButton(
key: const ValueKey('codelessly-toast-close'),
onPressed: _close,
tooltip: 'Close message',
iconSize: 16,
visualDensity: VisualDensity.compact,
color: textColor,
icon: const Icon(Icons.close),
),
],
),
);
Expand All @@ -243,20 +265,29 @@ class _ToastViewState extends State<_ToastView> {
offset: _visible ? Offset.zero : Offset(0, top ? -0.25 : 0.25),
duration: _fade,
curve: Curves.easeOutCubic,
child: Center(
child: Align(
alignment: switch (action.alignment) {
ToastAlignment.start => Alignment.centerLeft,
ToastAlignment.end => Alignment.centerRight,
ToastAlignment.center ||
ToastAlignment.stretch => Alignment.center,
},
child: Material(type: MaterialType.transparency, child: toast),
),
),
),
);

return Positioned(
left: 16,
right: 16,
left: action.sideMargin,
right: action.sideMargin,
top: top ? safe.top + 16 : null,
bottom: top ? null : action.bottomOffset,
// Only a toast with a button takes taps; the rest passes them through.
child: IgnorePointer(ignoring: !hasButton || !_visible, child: toast),
child: IgnorePointer(
ignoring: !(hasButton || hasClose) || !_visible,
child: toast,
),
);
}
}
121 changes: 108 additions & 13 deletions lib/src/functions/functions_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -571,9 +571,21 @@ class FunctionsRepository {
barrierDismissible: action.barrierDismissible,
barrierColor: action.barrierColor?.toFlutterColor(),
showCloseButton: action.showCloseButton,
placement: action.placement,
anchorGap: action.anchorGap,
);
}

/// The global rectangle of the widget [context] belongs to, if it is laid
/// out.
static Rect? _globalRectOf(BuildContext context) {
final RenderObject? box = context.mounted
? context.findRenderObject()
: null;
if (box is! RenderBox || !box.hasSize || !box.attached) return null;
return box.localToGlobal(Offset.zero) & box.size;
}

/// The id of the published layout that holds the canvas [canvasId], if any.
static String? layoutIdForCanvas(Codelessly codelessly, String canvasId) =>
codelessly.dataManager.publishModel?.layouts.values
Expand All @@ -590,6 +602,8 @@ class FunctionsRepository {
bool barrierDismissible = true,
Color? barrierColor,
bool showCloseButton = false,
DialogPlacement placement = DialogPlacement.center,
double anchorGap = 6,
}) async {
final ScopedValues scopedValues = ActionChain.scopedValuesOf(context);
final parsedParams = substituteVariablesInMap(params, scopedValues);
Expand Down Expand Up @@ -632,21 +646,94 @@ class FunctionsRepository {
// A chain triggered in the dialog can finish on this layout after the
// dialog closes (e.g. Navigate back, then a toast).
final List<BuildContext> openerHosts = ActionChainOpener.hostsFor(context);
final Object? result = await showDialog<Object?>(
context: context,
useRootNavigator: false,
barrierDismissible: barrierDismissible,
barrierColor: barrierColor,
routeSettings: RouteSettings(arguments: parsedParams),
builder: (context) => ActionChainOpener(
openerHosts: openerHosts,
child: CodelesslyDialogWidget(
showCloseButton: showCloseButton,
builder: (context) =>
CodelesslyWidget(codelessly: codelessly, layoutID: layoutId),
),
Widget dialog(BuildContext context) => ActionChainOpener(
openerHosts: openerHosts,
child: CodelesslyDialogWidget(
showCloseButton: showCloseButton,
builder: (context) =>
CodelesslyWidget(codelessly: codelessly, layoutID: layoutId),
),
);

final Size screen = MediaQuery.sizeOf(context);
final DialogPlacement effective = switch (placement) {
DialogPlacement.adaptive =>
screen.width < 600
? DialogPlacement.bottomSheet
: DialogPlacement.center,
_ => placement,
};
final Rect? anchor = effective == DialogPlacement.anchored
? _globalRectOf(context)
: null;

final Future<Object?> shown = switch (effective) {
DialogPlacement.bottomSheet => showGeneralDialog<Object?>(
context: context,
useRootNavigator: false,
barrierDismissible: barrierDismissible,
barrierLabel: 'Close',
barrierColor: barrierColor ?? Colors.black54,
transitionDuration: const Duration(milliseconds: 250),
routeSettings: RouteSettings(arguments: parsedParams),
pageBuilder: (context, _, _) => CodelesslySheetPlacement(
child: ActionChainOpener(
openerHosts: openerHosts,
child: CanvasSizingScope(
sizing: CanvasSizing.fill,
child: CodelesslyWidget(
codelessly: codelessly,
layoutID: layoutId,
),
),
),
),
transitionBuilder: (context, animation, _, child) => SlideTransition(
position: Tween(begin: const Offset(0, 1), end: Offset.zero).animate(
CurvedAnimation(parent: animation, curve: Curves.easeOutCubic),
),
child: child,
),
),
DialogPlacement.anchored when anchor != null =>
showGeneralDialog<Object?>(
context: context,
useRootNavigator: false,
barrierDismissible: barrierDismissible,
barrierLabel: 'Close',
barrierColor: barrierColor ?? Colors.transparent,
transitionDuration: const Duration(milliseconds: 120),
routeSettings: RouteSettings(arguments: parsedParams),
pageBuilder: (context, _, _) => CodelesslyAnchoredPlacement(
anchor: anchor,
gap: anchorGap,
child: ActionChainOpener(
openerHosts: openerHosts,
child: CanvasSizingScope(
sizing: CanvasSizing.design,
child: Material(
type: MaterialType.transparency,
child: CodelesslyWidget(
codelessly: codelessly,
layoutID: layoutId,
),
),
),
),
),
transitionBuilder: (context, animation, _, child) =>
FadeTransition(opacity: animation, child: child),
),
_ => showDialog<Object?>(
context: context,
useRootNavigator: false,
barrierDismissible: barrierDismissible,
barrierColor: barrierColor,
routeSettings: RouteSettings(arguments: parsedParams),
builder: dialog,
),
};
final Object? result = await shown;
if (context.mounted) {
codelessly.notifyNavigationListeners(
context,
Expand Down Expand Up @@ -1138,6 +1225,14 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json.
action.variable.name,
);
if (variableNotifier == null) return false;
if (variableNotifier.value.isComputed) {
DebugLogger.instance.printInfo(
'Variable ${action.variable.name} is computed from a formula and '
"can't be set.",
name: name,
);
return false;
}

String newValue = substituteNewValue(
action.newValue,
Expand Down
2 changes: 2 additions & 0 deletions lib/src/transformers/node_transformers/node_transformers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export 'passive_navigation_bar_transformer.dart';
export 'passive_page_view_transformer.dart';
export 'passive_placeholder_transformer.dart';
export 'passive_progressbar_transformer.dart';
export 'passive_gauge_transformer.dart';
export 'passive_bar_chart_transformer.dart';
export 'passive_radio_transformer.dart';
export 'passive_rectangle_transformer.dart';
export 'passive_row_column_transformer.dart';
Expand Down
Loading
Loading