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

@ -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,