feat(editor): five distinct type colours + wired/total badge in step header

Two operator-visible improvements:

 - Five payload types now get five distinct hues that
   survive both light and dark themes:
     text   → blue (0xFF42A5F5)
     bytes  → deep orange (0xFFFF7043)
     json   → purple (0xFFAB47BC)
     file   → green (0xFF66BB6A)
     number → amber (0xFFFFCA28)
   Previously bytes + file shared a colour and the palette
   collapsed under custom theme plugins. Hardcoded hues
   keep them readable everywhere.

 - Step headers now carry a small "wired/total" badge.
   When all with-fields carry a `$src.field` reference,
   the badge collapses to a green check icon — the visual
   "this module is fully wired" signal Stefan asked for.
   Partial-wiring shows e.g. "3/5" in a pill matching the
   step accent. Endpoint nodes don't render the badge
   (ratio doesn't apply there).

Computed live from graph.edges + step.with_ values, so the
indicator updates the moment an operator drags a
connection in or out.

Version 0.5.0 -> 0.5.1.

Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
flemming-it 2026-06-01 16:47:29 +02:00
parent 099cd182b9
commit e4e2d45374
3 changed files with 70 additions and 10 deletions

View file

@ -311,6 +311,14 @@ class _FlowCanvasState extends State<FlowCanvas> {
final selected = widget.controller.selectedStepId == step.id;
final raw = widget.controller.stepStatuses[step.id] ?? StepRunStatus.idle;
final status = _toNodeStatus(raw);
// How many with-fields carry a wired-up `$src.field`
// expression. Drives the header's "n/total" badge so the
// operator can see at a glance whether the module is
// fully connected.
final wired = step.with_.values
.whereType<Object>()
.where((v) => _isWiredExpression(v.toString()))
.length;
return Positioned(
left: pos.x,
top: pos.y,
@ -319,6 +327,7 @@ class _FlowCanvasState extends State<FlowCanvas> {
title: step.id,
subtitle: step.use,
inputPortLabels: step.with_.keys.toList(),
wiredCount: wired,
kind: kindForStep(step),
selected: selected,
status: status,
@ -591,20 +600,26 @@ class _FlowCanvasState extends State<FlowCanvas> {
}
Color _typeAccent(String type, ThemeData theme) {
final cs = theme.colorScheme;
// Five distinct hues for the five payload types F-Delta-I
// flows declare today. Picked from a high-contrast set
// that survives both light and dark themes; intentionally
// NOT pulled exclusively from the theme's primary /
// secondary / tertiary slots because those collide once
// a custom theme plugin is active.
switch (type) {
case 'text':
return cs.primary;
return const Color(0xFF42A5F5); // blue
case 'bytes':
case 'file':
return cs.tertiary;
return const Color(0xFFFF7043); // deep orange
case 'json':
return cs.secondary;
return const Color(0xFFAB47BC); // purple
case 'file':
return const Color(0xFF66BB6A); // green
case 'number':
case 'integer':
return Colors.amber.shade400;
return const Color(0xFFFFCA28); // amber
default:
return cs.onSurfaceVariant;
return theme.colorScheme.onSurfaceVariant;
}
}
@ -786,7 +801,6 @@ class _FlowCanvasState extends State<FlowCanvas> {
return (best - portCenter).distance < 0.5;
}
void _finalizeDraft() {
final draft = _draft;
setState(() => _draft = null);

View file

@ -117,6 +117,14 @@ class FlowNode extends StatelessWidget {
/// "must wire" and this is empty.
final Set<String> optionalLabels;
/// How many of [inputPortLabels] are currently wired to an
/// upstream source. When equal to the total count, the
/// header paints a "complete" check mark; otherwise it
/// renders the ratio (e.g. "3/5") so the operator sees
/// at a glance what's still to wire. Set to null on
/// endpoint nodes where the ratio doesn't apply.
final int? wiredCount;
/// Right-click handler on the node body. Canvas wires this
/// to a popup menu at the cursor position (Duplicate /
/// Delete / Disconnect inputs etc.).
@ -136,6 +144,7 @@ class FlowNode extends StatelessWidget {
this.kind = NodeVisualKind.module,
this.portSide = NodePortSide.left,
this.optionalLabels = const {},
this.wiredCount,
this.selected = false,
this.status = FlowNodeStatus.idle,
this.onTap,
@ -224,7 +233,12 @@ class FlowNode extends StatelessWidget {
),
),
),
if (status != FlowNodeStatus.idle) _statusDot(theme),
if (wiredCount != null && inputPortLabels.isNotEmpty)
_wiredBadge(theme, accent),
if (status != FlowNodeStatus.idle) ...[
const SizedBox(width: 4),
_statusDot(theme),
],
],
),
),
@ -313,6 +327,38 @@ class FlowNode extends StatelessWidget {
);
}
Widget _wiredBadge(ThemeData theme, Color accent) {
final total = inputPortLabels.length;
final wired = wiredCount ?? 0;
final complete = wired >= total && total > 0;
if (complete) {
return Padding(
padding: const EdgeInsets.only(left: 4),
child: Icon(Icons.check_circle, size: 14, color: Colors.green.shade400),
);
}
return Padding(
padding: const EdgeInsets.only(left: 4),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
decoration: BoxDecoration(
color: accent.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: accent.withValues(alpha: 0.4)),
),
child: Text(
'$wired/$total',
style: theme.textTheme.labelSmall?.copyWith(
fontFamily: 'monospace',
fontSize: 9,
fontWeight: FontWeight.w600,
color: accent,
),
),
),
);
}
Widget _statusDot(ThemeData theme) {
final color = switch (status) {
FlowNodeStatus.running => theme.colorScheme.primary,

View file

@ -1,6 +1,6 @@
name: fai_studio_flow_editor
description: Swappable inline YAML editor for F∆I Studio flows.
version: 0.5.0
version: 0.5.1
publish_to: 'none'
repository: https://git.flemming.ai/fai/studio-flow-editor