// FaiErrorBox — selectable monospace error / output block with // a small copy-to-clipboard button in the top-right corner. // // Used wherever the operator might want to paste daemon stderr // into a chat or bug report: the update banner, install // progress dialog, system-AI editor, audit-clear failures, etc. // SelectableText alone works for keyboard users but the copy // affordance has to be explicit so trackpad operators see it. import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../l10n/app_localizations.dart'; import '../theme/theme.dart'; import '../theme/tokens.dart'; class FaiErrorBox extends StatefulWidget { /// The text the operator wants to read (and copy). Rendered /// monospace, selectable, multi-line. final String text; /// When true, the box border + foreground colour come from the /// error palette. False renders neutral chrome — useful for /// success / informational copy that still wants the copy /// button. final bool isError; /// Optional max-height before the content scrolls inside the /// box. Falls back to no constraint when null so short /// messages don't get a useless scrollbar. final double? maxHeight; const FaiErrorBox({ super.key, required this.text, this.isError = false, this.maxHeight, }); @override State createState() => _FaiErrorBoxState(); } class _FaiErrorBoxState extends State { bool _justCopied = false; Future _copy() async { await Clipboard.setData(ClipboardData(text: widget.text)); if (!mounted) return; setState(() => _justCopied = true); Future.delayed(const Duration(seconds: 2), () { if (mounted) setState(() => _justCopied = false); }); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final l = AppLocalizations.of(context)!; final accent = widget.isError ? theme.colorScheme.error : theme.colorScheme.outlineVariant; final fg = widget.isError ? theme.colorScheme.error : theme.colorScheme.onSurface; return Container( width: double.infinity, padding: const EdgeInsets.fromLTRB( FaiSpace.sm, FaiSpace.xs, FaiSpace.xs, FaiSpace.sm, ), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(FaiRadius.sm), border: Border.all(color: accent.withValues(alpha: 0.4)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ // Copy button sits above the text on its own row so // long messages never collide with it. Compact enough // not to dominate short single-line messages. Tooltip( message: _justCopied ? l.buttonCopied : l.buttonCopy, child: IconButton( icon: Icon( _justCopied ? Icons.check : Icons.content_copy, size: 14, ), visualDensity: VisualDensity.compact, padding: const EdgeInsets.all(4), constraints: const BoxConstraints(), onPressed: _copy, ), ), ConstrainedBox( constraints: BoxConstraints( maxHeight: widget.maxHeight ?? double.infinity, ), child: Scrollbar( child: SingleChildScrollView( child: SelectableText( widget.text, style: FaiTheme.mono(size: 11, color: fg), ), ), ), ), ], ), ); } }