Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ad0fe8a
Render gauge and chart nodes and repeating gradients
claude Oct 22, 2026
394f7fb
Evaluate computed variables; add object and slicing formula functions
claude Oct 22, 2026
d9d30b5
Design canvas: list rows preview their own item; gauge and chart take…
claude Oct 23, 2026
86f65ae
Formulas: concat, range, thousands, duration; progress bar takes prev…
claude Oct 23, 2026
041835b
Apply a node's pulse: PulseOpacity fades it between its faint and ful…
claude Oct 23, 2026
4c943a0
Computed choice variables read as text
claude Oct 23, 2026
7e2c55c
Dropdown takes preview values so a design canvas shows its bound item…
claude Oct 23, 2026
4f28018
Lint: braces around a single-statement if; single quotes in a formula…
claude Oct 23, 2026
8b2b0bc
Changelog: unreleased changes
claude Oct 23, 2026
c1a554c
Slider takes preview values so a design canvas shows its bound value
claude Oct 23, 2026
c51a011
Responsive dialogs follow the screen; empty formula previews
claude Oct 24, 2026
2e3455a
Chart timeline: draw the now marker in nowColor
claude Oct 24, 2026
e985baf
Fire pointerDown and pointerUp reactions
claude Oct 24, 2026
01ecc24
Draw rectangular slider thumbs
claude Oct 24, 2026
e861b45
Toast accent edge, corner radius and max width
claude Oct 24, 2026
abe44b9
Bindable obscureText; fixed-height password fields obscure
claude Oct 24, 2026
78df8be
Anchored dialogs open as popovers next to their trigger
claude Oct 24, 2026
795fe76
Render wrapping rows as Wrap
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
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,47 @@
## Unreleased

### Nodes

- Renders `gauge` and `chart` nodes (`PassiveGaugeWidget`,
`PassiveChartWidget`); taps on a chart select a category and fire its
`changed` trigger with the index.
- Repeating and mirrored linear, radial and sweep gradients.
- A node's `pulse` fades its opacity in a loop (`PulseOpacity`).
- A row with `wrap` renders as a `Wrap`: children keep their design size
and break onto new lines.
- A dialog anchored to its trigger opens as a popover next to the node that
ran the action, flipping to the other side when it doesn't fit and kept
inside the safe area (`CanvasSizing.content`).
- A text field's `obscureText` can be bound to a variable or condition, and a
one-line field of fixed height obscures its text (it expanded to its
height, which dropped the line limit and so the obscuring).
- Toasts take an accent edge, corner radius and max width
(`ShowToastAction.accentColor`, `cornerRadius`, `maxWidth`).
- Rectangular slider thumbs (`RectSliderThumbShape`) with a border and a
grip line.
- `pointerDown` and `pointerUp` reactions fire (a cancelled pointer counts
as up), so a press and hold can start on down and stop on release.
- A dialog whose canvas holds a responsive layout fills the screen and
shows the layout for the screen's width, not the canvas's design width.
- On the design canvas, a formula that yields empty text shows nothing
instead of a placeholder.

### Variables and formulas

- Computed variables: a variable whose value is a `${= ...}` formula reads
as the formula's result, typed, in bindings, formulas and conditions.
Results are cached until a variable they read changes, and bindings
update when one does. Cycles stop; SetVariable leaves them alone.
- Formula functions `obj`, `set`, `get`, `take`, `skip`, `indexOf`,
`split`, `pad`, `concat`, `range`, `thousands` and `duration`.

### Design canvas

- `VariablePreview.resolve(node, context)` gives a list row's own item to
the editor's resolver, so each row previews its data.
- Gauge, chart, progress bar and dropdown widgets take `scopedValues`, so a
design canvas can show their bindings with its variable defaults.

## 8.2.0

### States
Expand Down
9 changes: 7 additions & 2 deletions lib/src/functions/app_toast.dart
Original file line number Diff line number Diff line change
Expand Up @@ -188,14 +188,19 @@ class _ToastViewState extends State<_ToastView> {
decoration: TextDecoration.none,
);

final Color? accent = action.accentColor?.toFlutterColor();
Widget toast = Container(
constraints: const BoxConstraints(minHeight: 44, maxWidth: 560),
constraints: BoxConstraints(minHeight: 44, maxWidth: action.maxWidth),
padding: EdgeInsets.fromLTRB(18, 10, hasButton ? 8 : 18, 10),
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(
hasButton ? theme.buttonRadius.clamp(8, 999) : 999,
action.cornerRadius ??
(hasButton ? theme.buttonRadius.clamp(8, 999) : 999),
),
border: accent == null
? null
: BorderDirectional(start: BorderSide(color: accent, width: 4)),
boxShadow: const [
BoxShadow(
color: Color(0x33000000),
Expand Down
78 changes: 65 additions & 13 deletions lib/src/functions/functions_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:url_launcher/url_launcher.dart';
import '../../codelessly_sdk.dart';
import '../logging/stat_tracker.dart';
import '../ui/codelessly_dialog_widget.dart';
import '../ui/codelessly_popover.dart';
import 'action_chain.dart';
import 'app_auth.dart';
import 'cloud_files.dart';
Expand Down Expand Up @@ -571,6 +572,10 @@ class FunctionsRepository {
barrierDismissible: action.barrierDismissible,
barrierColor: action.barrierColor?.toFlutterColor(),
showCloseButton: action.showCloseButton,
anchor: action.anchor,
placement: action.placement,
anchorGap: action.anchorGap,
anchorOffset: action.anchorOffset,
);
}

Expand All @@ -583,13 +588,22 @@ class FunctionsRepository {
/// Shows the canvas [canvasId] as a dialog and completes with what it was
/// closed with: the params of the "Navigate back" action that closed it, or
/// null when it was dismissed.
///
/// With [anchor] set to [DialogAnchor.trigger] the dialog is a popover next
/// to [context]'s widget, the node that ran the action, at [placement]
/// (see [CodelesslyPopover]). It is centered when that widget has no laid
/// out box.
static Future<Object?> showLayoutDialog(
BuildContext context, {
required String canvasId,
Map<String, dynamic> params = const {},
bool barrierDismissible = true,
Color? barrierColor,
bool showCloseButton = false,
DialogAnchor anchor = DialogAnchor.none,
DialogPlacement placement = DialogPlacement.belowStart,
double anchorGap = 4,
double anchorOffset = 0,
}) async {
final ScopedValues scopedValues = ActionChain.scopedValuesOf(context);
final parsedParams = substituteVariablesInMap(params, scopedValues);
Expand Down Expand Up @@ -632,21 +646,51 @@ 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),
),
// The context an action runs with is the widget of the node that ran it,
// which an anchored dialog opens next to.
final Rect? anchorRect = anchor == DialogAnchor.trigger
? CodelesslyPopover.anchorRectOf(
context,
Navigator.of(context, rootNavigator: false),
)
: null;
if (anchor == DialogAnchor.trigger && anchorRect == null) {
DebugLogger.instance.printInfo(
'The trigger of the dialog [$canvasId] has no laid out box. Showing '
'the dialog centered.',
name: name,
);
}
Widget dialog(BuildContext context) => ActionChainOpener(
openerHosts: openerHosts,
child: CodelesslyDialogWidget(
showCloseButton: showCloseButton,
anchored: anchorRect != null,
builder: (context) =>
CodelesslyWidget(codelessly: codelessly, layoutID: layoutId),
),
);
final RouteSettings routeSettings = RouteSettings(arguments: parsedParams);
final Object? result = anchorRect != null
? await CodelesslyPopover.show<Object?>(
context: context,
anchorRect: anchorRect,
placement: placement,
gap: anchorGap,
offset: anchorOffset,
barrierDismissible: barrierDismissible,
barrierColor: barrierColor,
routeSettings: routeSettings,
builder: dialog,
)
: await showDialog<Object?>(
context: context,
useRootNavigator: false,
barrierDismissible: barrierDismissible,
barrierColor: barrierColor,
routeSettings: routeSettings,
builder: dialog,
);
if (context.mounted) {
codelessly.notifyNavigationListeners(
context,
Expand Down Expand Up @@ -1138,6 +1182,14 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json.
action.variable.name,
);
if (variableNotifier == null) return false;
// A computed variable's value is its formula; there's nothing to set.
if (variableNotifier.value.isComputed) {
DebugLogger.instance.printInfo(
'Not setting ${action.variable.name}: it is computed by a formula.',
name: 'FunctionsRepository',
);
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 @@ -5,6 +5,7 @@ export 'passive_app_bar_transformer.dart';
export 'passive_badge_transformer.dart';
export 'passive_button_transformer.dart';
export 'passive_canvas_transformer.dart';
export 'passive_chart_transformer.dart';
export 'passive_checkbox_transformer.dart';
export 'passive_chip_transformer.dart';
export 'passive_divider_transformer.dart';
Expand All @@ -13,6 +14,7 @@ export 'passive_embedded_video_transformer.dart';
export 'passive_expansion_tile_transformer.dart';
export 'passive_external_component_transformer.dart';
export 'passive_floating_action_button_transformer.dart';
export 'passive_gauge_transformer.dart';
export 'passive_grid_view_transformer.dart';
export 'passive_icon_transformer.dart';
export 'passive_list_tile_transformer.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ class PassiveButtonWidget extends StatelessWidget {
final List<TextSpan> spans = buttonLabelTemplateOf(node)
.splitMap(
variableSyntaxIdentifierRegex,
onMatch: (match) => VariableSpan(variable: match[0]!, node: node),
onMatch: (match) =>
VariableSpan(variable: match[0]!, node: node, context: context),
onNonMatch: (text) => TextSpan(text: text),
)
.toList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,20 @@ class PassiveCanvasTransformer extends NodeWidgetTransformer<CanvasNode> {
// );
// }

/// Whether [node]'s layout is responsive: a variance with breakpoints
/// within its body's first levels, where `layouts.createResponsive` puts
/// one.
bool hasResponsiveLayout(CanvasNode node) {
bool check(String id, int depth) {
final BaseNode child = getNode(id);
if (child is VarianceNode && child.isResponsive) return true;
if (depth == 0 || child is! ChildrenMixin) return false;
return child.children.any((id) => check(id, depth - 1));
}

return node.children.any((id) => check(id, 2));
}

@override
Widget buildWidget(
CanvasNode node,
Expand All @@ -377,6 +391,7 @@ class PassiveCanvasTransformer extends NodeWidgetTransformer<CanvasNode> {
settings: settings,
builder: (context) => CanvasViewport(
node: node,
responsive: hasResponsiveLayout(node),
child: switch (node.scaleMode) {
ScaleMode.responsive => _wrapInScaffoldForResponsive(
context: context,
Expand Down Expand Up @@ -440,6 +455,12 @@ enum CanvasSizing {
/// given. Used where the host only centers the layout and lets it choose
/// its size, such as a dialog.
design,

/// Like [design], and a canvas holding a responsive layout keeps its
/// design size too instead of filling the space. Used for a popover next
/// to the node that opened it, which is as big as it was designed (or as
/// tall as its content) wherever it opens.
content,
}

/// Sets the [CanvasSizing] of the canvases below it. Without one, canvases
Expand Down Expand Up @@ -497,7 +518,17 @@ class CanvasViewport extends StatelessWidget {
final CanvasNode node;
final Widget child;

const CanvasViewport({super.key, required this.node, required this.child});
/// Whether the canvas holds a responsive layout. Such a canvas fills the
/// space it is given even under [CanvasSizing.design], so its layout
/// follows the screen rather than the breakpoint it was last designed at.
final bool responsive;

const CanvasViewport({
super.key,
required this.node,
required this.child,
this.responsive = false,
});

/// The canvas box for [constraints] under [sizing], per axis. A null
/// height leaves the height to the canvas content (see
Expand All @@ -506,6 +537,7 @@ class CanvasViewport extends StatelessWidget {
CanvasNode node,
BoxConstraints constraints, {
CanvasSizing sizing = CanvasSizing.fill,
bool responsive = false,
}) {
// A canvas whose height is its content's. A scaffold needs a bounded
// height, so a canvas with an app bar, navigation bar or FAB takes its
Expand All @@ -514,7 +546,8 @@ class CanvasViewport extends StatelessWidget {
node.verticalFit == SizeFit.shrinkWrap &&
node.scaleMode == ScaleMode.responsive &&
!node.isAScaffold;
if (sizing == CanvasSizing.design) {
if ((sizing == CanvasSizing.design && !responsive) ||
sizing == CanvasSizing.content) {
final double width = constraints.constrainWidth(node.basicBoxLocal.width);
if (contentHeight) return (width: width, height: null);
final double height = node.scaleMode == ScaleMode.autoScale
Expand Down Expand Up @@ -543,7 +576,12 @@ class CanvasViewport extends StatelessWidget {
final CanvasSizing sizing = CanvasSizingScope.of(context);
return LayoutBuilder(
builder: (context, constraints) {
final box = resolve(node, constraints, sizing: sizing);
final box = resolve(
node,
constraints,
sizing: sizing,
responsive: responsive,
);
// The width the canvas content is laid out at: the canvas box for a
// responsive canvas, the design width for an autoScale one (its
// content is laid out at design size and then scaled).
Expand Down
Loading
Loading