feat(editor): port geometry fix + type colours + endpoint editor + unsaved badge

Four operator-visible improvements in one cut.

Port geometry (the "connection dots don't line up with the
labels" bug):
 - Header now has a fixed two-line structure (title + 16-px
   subtitle slot) regardless of node kind, so the body's
   port-row Y-offsets are identical across step nodes and
   endpoint nodes.
 - NodeGeometry constants rewritten so the inline 8-px dot
   inside each port row and the 16-px canvas-side dot both
   compute to the same Y. Port rows now anchor where the
   eye expects them.

Port type colours:
 - Inputs endpoint labels carry `name: type` (e.g.
   `doc: bytes`), so the FlowNode body parses the type and
   tints the inline dot per type: text → primary,
   bytes/file → tertiary, json → secondary, number → amber,
   unknown → muted. Step input ports stay muted until a
   future commit can read module manifests for type info.

Endpoint editor:
 - Inputs and outputs nodes are now selectable. Selecting
   opens a dedicated editor in the properties panel:
   add / rename / retype / remove entries graphically
   instead of forcing the operator into the text tab.
   Inputs editor offers a Type dropdown
   (text / bytes / json / file / number);
   outputs editor exposes name + expression
   (e.g. `$step.field`).

Unsaved badge:
 - The toolbar's tiny 8-px dirty dot was easy to miss.
   Replace it with a coloured "unsaved" / "ungespeichert"
   chip + colour the file name itself in the primary accent
   when dirty. Operators see at a glance that a flow has
   unsaved changes, which matters because the Discard
   confirmation prompt won't fire if they don't realise
   they made changes.

Version 0.3.0 -> 0.4.0 — first new editor feature surface
since 0.3.

Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
flemming-it 2026-06-01 16:31:28 +02:00
parent 296e5bfd01
commit fb8892687d
6 changed files with 761 additions and 81 deletions

View file

@ -612,17 +612,44 @@ class _Toolbar extends StatelessWidget {
style: theme.textTheme.titleSmall?.copyWith(
fontFamily: 'monospace',
fontWeight: FontWeight.w600,
// Dirty file name renders in the primary accent
// so the operator sees "this file has unsaved
// changes" at a glance even when their eye is
// elsewhere in the UI.
color: dirty ? theme.colorScheme.primary : null,
),
),
if (dirty)
Padding(
padding: const EdgeInsets.only(left: FaiSpace.xs),
child: Icon(
Icons.circle,
size: 8,
color: theme.colorScheme.primary,
if (dirty) ...[
const SizedBox(width: FaiSpace.xs),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(FaiRadius.sm),
border: Border.all(
color: theme.colorScheme.primary.withValues(alpha: 0.4),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.fiber_manual_record,
size: 8,
color: theme.colorScheme.primary,
),
const SizedBox(width: 4),
Text(
strings.unsaved,
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
const Spacer(),
FilledButton.tonalIcon(
onPressed: onNew,