diff --git a/lib/src/flow_editor_page.dart b/lib/src/flow_editor_page.dart index d2a9baf..3b51a1d 100644 --- a/lib/src/flow_editor_page.dart +++ b/lib/src/flow_editor_page.dart @@ -592,18 +592,36 @@ outputs: ); return CodeTheme( data: CodeThemeData(styles: _yamlStyle(theme)), - child: CodeField( - controller: _controller.codeController, - textStyle: mono, - expands: true, - minLines: null, - maxLines: null, - gutterStyle: GutterStyle( - textStyle: mono.copyWith(color: theme.colorScheme.onSurfaceVariant), - background: theme.colorScheme.surfaceContainer, - showLineNumbers: true, - ), - background: theme.colorScheme.surface, + child: Column( + children: [ + Expanded( + child: CodeField( + controller: _controller.codeController, + textStyle: mono, + expands: true, + minLines: null, + maxLines: null, + gutterStyle: GutterStyle( + textStyle: mono.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + background: theme.colorScheme.surfaceContainer, + showLineNumbers: true, + // Disable the built-in error column entirely. + // The Icons.cancel pin is hard-positioned inside + // a 16-px slot, and its mouse-hover popup paints + // OVER the code area instead of beside it — the + // overlap was the operator-visible glitch in + // editor 0.15.1. Issues remain surfaced via the + // wavy underline (FlowYamlCodeController) plus + // the bottom diagnostic strip below. + showErrors: false, + ), + background: theme.colorScheme.surface, + ), + ), + _DiagnosticStrip(controller: _controller.codeController), + ], ), ); } @@ -1105,3 +1123,205 @@ class _NewFlowDialogState extends State<_NewFlowDialog> { ); } } + +/// Bottom diagnostic strip — single-line "X issues · {first}" +/// summary that opens into a per-line list on tap. Replaces the +/// gutter's hard-positioned error pin so the popup never paints +/// over the code area, and gives the operator a stable surface +/// to scan even when the wavy underline sits off-screen. +/// +/// The strip listens to the CodeController; it rebuilds on every +/// analyzer pass without polling. +class _DiagnosticStrip extends StatefulWidget { + final CodeController controller; + const _DiagnosticStrip({required this.controller}); + + @override + State<_DiagnosticStrip> createState() => _DiagnosticStripState(); +} + +class _DiagnosticStripState extends State<_DiagnosticStrip> { + bool _expanded = false; + + @override + void initState() { + super.initState(); + widget.controller.addListener(_onChange); + } + + @override + void dispose() { + widget.controller.removeListener(_onChange); + super.dispose(); + } + + void _onChange() { + if (mounted) setState(() {}); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final issues = widget.controller.analysisResult.issues; + if (issues.isEmpty) { + return Container( + height: 22, + padding: const EdgeInsets.symmetric(horizontal: 12), + alignment: Alignment.centerLeft, + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainer, + border: Border( + top: BorderSide(color: theme.colorScheme.outlineVariant), + ), + ), + child: Text( + 'No issues', + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 10, + letterSpacing: 0.4, + color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.6), + ), + ), + ); + } + + final errorCount = issues.where((i) => i.type == IssueType.error).length; + final warnCount = issues.where((i) => i.type == IssueType.warning).length; + final tone = errorCount > 0 + ? theme.colorScheme.error + : const Color(0xFFEF6C00); // amber/orange for warnings + + return Material( + color: theme.colorScheme.surfaceContainer, + child: InkWell( + onTap: () => setState(() => _expanded = !_expanded), + child: Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide(color: theme.colorScheme.outlineVariant), + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: tone, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Text( + _summary(errorCount, warnCount), + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 11, + fontWeight: FontWeight.w600, + color: tone, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'L${issues.first.line + 1}: ${issues.first.message}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 11, + color: theme.colorScheme.onSurface, + ), + ), + ), + Icon( + _expanded ? Icons.expand_more : Icons.expand_less, + size: 16, + color: theme.colorScheme.onSurfaceVariant, + ), + ], + ), + if (_expanded) ...[ + const SizedBox(height: 6), + Divider(height: 1, color: theme.colorScheme.outlineVariant), + const SizedBox(height: 6), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 140), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (final issue in issues) + Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 6, + height: 6, + margin: const EdgeInsets.only(top: 5, right: 8), + decoration: BoxDecoration( + color: switch (issue.type) { + IssueType.error => + theme.colorScheme.error, + IssueType.warning => + const Color(0xFFEF6C00), + IssueType.info => + theme.colorScheme.primary, + }, + shape: BoxShape.circle, + ), + ), + SizedBox( + width: 42, + child: Text( + 'L${issue.line + 1}', + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 11, + color: + theme.colorScheme.onSurfaceVariant, + ), + ), + ), + Expanded( + child: Text( + issue.message, + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 11, + color: theme.colorScheme.onSurface, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ], + ), + ), + ), + ); + } + + String _summary(int errors, int warnings) { + final parts = []; + if (errors > 0) parts.add('$errors error${errors == 1 ? '' : 's'}'); + if (warnings > 0) { + parts.add('$warnings warning${warnings == 1 ? '' : 's'}'); + } + return parts.join(' · '); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index b354a4b..1af70d8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: fai_studio_flow_editor description: Swappable inline YAML editor for F∆I Studio flows. -version: 0.15.1 +version: 0.16.0 publish_to: 'none' repository: https://git.flemming.ai/fai/studio-flow-editor