fix(editor): solid-filled connected port + larger dot + tooltip fallback

Three issues from Stefan's screenshot:

1. Connected port dots rendered as a ring with a small
   surface-coloured pin in the centre — read as a hollow
   bullseye target rather than 'this port is wired'.
   Stefan asked for a plain filled circle: removed the
   inner pin so connected = solid disc, disconnected =
   ring with surface fill. One unambiguous visual switch.

2. Port dots bumped from 12 px to 14 px diameter. At 50 %
   zoom (and under the wider cards we now draw) the 12 px
   outline was sinking into the canvas background.

3. Tooltips now appear on every visible port label, not
   only on those whose ModuleSpec carries a description.
   Fallback chain:
     - manifest description in active locale
     - manifest description in English (peer)
     - 'fieldname · type' when the field is declared but
       the description is missing
     - bare 'fieldname' for implicit (YAML-only) fields
   Operators always see something on hover, useful when
   the hub binary is older than the per-field ModuleInfo
   RPC and the editor is running in implicit-only mode.

Bumps fai_studio_flow_editor to 0.10.3.

Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
flemming-it 2026-06-02 00:56:14 +02:00
parent 3a6e92fe09
commit a1d25fd48a
3 changed files with 57 additions and 54 deletions

View file

@ -746,16 +746,34 @@ class _FlowCanvasState extends State<FlowCanvas>
final spec = _specForStep(step); final spec = _specForStep(step);
final inputLabels = _inputLabelsForStep(step); final inputLabels = _inputLabelsForStep(step);
final outputLabels = _outputLabelsForStep(step); final outputLabels = _outputLabelsForStep(step);
// Tooltips: every visible label gets one. When the
// manifest carries a locale description, use it. Else
// fall back to `name (type)` for declared fields whose
// description is missing, or to the bare field name for
// implicit (YAML-only) fields. Operators always see
// something on hover useful at least to confirm the
// exact field they're pointing at.
final tooltips = <String, String>{}; final tooltips = <String, String>{};
final loc = widget.locale == FlowEditorLocale.de ? 'de' : 'en';
final declaredFields = <String, ModuleField>{};
if (spec != null) { if (spec != null) {
final loc = widget.locale == FlowEditorLocale.de ? 'de' : 'en'; for (final f in [...spec.inputs, ...spec.outputs]) {
for (final f in spec.inputs) { declaredFields[f.name] = f;
final d = f.descriptionFor(loc);
if (d != null) tooltips[f.name] = d;
} }
for (final f in spec.outputs) { }
for (final label in [...inputLabels, ...outputLabels]) {
final f = declaredFields[label];
if (f != null) {
final d = f.descriptionFor(loc); final d = f.descriptionFor(loc);
if (d != null) tooltips[f.name] = d; if (d != null && d.isNotEmpty) {
tooltips[label] = d;
} else if (f.type.isNotEmpty) {
tooltips[label] = '$label · ${f.type}';
} else {
tooltips[label] = label;
}
} else {
tooltips[label] = label;
} }
} }
final cardHeight = NodeGeometry.heightFor( final cardHeight = NodeGeometry.heightFor(
@ -1714,50 +1732,32 @@ class _FlowCanvasState extends State<FlowCanvas>
onLongPressStart: onContextMenu == null onLongPressStart: onContextMenu == null
? null ? null
: (details) => onContextMenu(details.globalPosition), : (details) => onContextMenu(details.globalPosition),
child: Stack( child: Container(
alignment: Alignment.center, // Connected = solid filled circle in accent.
children: [ // Dangling = clear ring with surface fill so the
// Outer ring defines the port's footprint. // empty socket is obvious. Drop-target halo
// Filled when connected (so the line "docks" // glows in addition. No inner pin Stefan
// into a visible target), surface-filled when // explicitly wants the connected variant to read
// dangling (a clear empty socket). Drop-target // as one continuous filled disc, not a ring with
// halo grows + glows. // a bullseye in it.
Container( decoration: BoxDecoration(
decoration: BoxDecoration( shape: BoxShape.circle,
shape: BoxShape.circle, color: filled ? accent : theme.colorScheme.surface,
color: filled ? accent : theme.colorScheme.surface, border: Border.all(
border: Border.all( color: accent,
color: accent, width: isClosest ? 2.5 : (isHovered ? 2.2 : 1.8),
width: isClosest ? 2.5 : (isHovered ? 2.2 : 1.8),
),
boxShadow: (isClosest || isHovered)
? [
BoxShadow(
color: accent.withValues(
alpha: isClosest ? 0.55 : 0.35,
),
blurRadius: isClosest ? 10 : 6,
),
]
: null,
),
), ),
// Inner pin tiny surface-coloured dot at the boxShadow: (isClosest || isHovered)
// centre when connected, giving the port the ? [
// "socket with a pin in it" look that reads as BoxShadow(
// an active electrical connector rather than a color: accent.withValues(
// free-floating indicator. Skipped on empty alpha: isClosest ? 0.55 : 0.35,
// ports so the hollow ring is unambiguous. ),
if (connected) blurRadius: isClosest ? 10 : 6,
Container( ),
width: 4, ]
height: 4, : null,
decoration: BoxDecoration( ),
color: theme.colorScheme.surface,
shape: BoxShape.circle,
),
),
],
), ),
), ),
), ),

View file

@ -83,10 +83,13 @@ class NodeGeometry {
/// without overlapping the label text. /// without overlapping the label text.
static const double portGutter = 18; static const double portGutter = 18;
/// Diameter of the canvas-side port dot sized to the /// Diameter of the canvas-side port dot. Sized so the
/// body's labelSmall row (~11 px text + 11 px breathing /// connected-vs-outlined state reads clearly at any zoom:
/// room) so the dot reads as the port without crowding. /// a 12 px outline tends to disappear into the background
static const double portDotSize = 12; /// at 50 % zoom; 14 px keeps the ring visible and gives
/// the filled (connected) variant enough mass to stand out
/// against the card shadow.
static const double portDotSize = 14;
/// Total card height for a node carrying [inputCount] inputs /// Total card height for a node carrying [inputCount] inputs
/// on the left + [outputCount] outputs on the right. The /// on the left + [outputCount] outputs on the right. The

View file

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