chain-studio/lib/widgets/fai_error_box.dart
flemming-it 3b5fb027c3 feat(studio): de-jargonize, reveal-in-OS, copyable errors, today carousel, AppBar alignment, F∆I window title (v0.34.0)
Sweeping pass against six user reports collected this session.

1. "Capabilities ist nicht deutsch, Chain auch nicht."
   The DE locale still leaked English vocabulary. Replaced
   "Capabilities" → "Fähigkeiten" and "Chain" / "Hash-Chain"
   → "Kette" / "Hash-Kette" everywhere — store search hint,
   recommended-source body, federated toast, doctor summary
   chain row, modules panel summary, MCP / n8n hints, the
   approvals history blurb. Wire-level identifiers
   (`chain.reset`) stay as code.

2. "Bei Fehlern unten muss man die auch ins clipboard
   kopieren können." New `FaiErrorBox` widget: selectable
   monospace block with a small copy-to-clipboard icon
   button that flips to a checkmark for two seconds after
   click. Applied to the Doctor update banner output and
   the Settings channel toast — the two places long
   stderr / stdout lands.

3. "Öffnen bei Log kann es nicht öffnen. Audit-DB auch
   nicht. PID auch nicht."
   Cause: `SystemActions.openInOs` shells out to `open` /
   `xdg-open` on file paths the OS has no default handler
   for (SQLite DB, PID file, log without an .ext that
   binds). New `revealInOs` uses `open -R` on macOS,
   `explorer /select,` on Windows, and the parent
   directory via `xdg-open` on Linux. Doctor's path rows
   carry an `isDirectory` flag that routes through the new
   `openOrReveal` so files reveal in Finder / Explorer
   instead of failing silently.

4. "Oben im Store könnte man diesen Redaktionshinweis auch
   so bauen, dass man mit pfeil nach rechts links auch
   weitere anzeigen kann."
   The Today-Hero became a carousel. Curated fallback
   list grew from one entry to four (public sources, the
   sandbox-by-default permission story, the hash-chained
   audit story, the air-gap-ready single-binary pitch).
   Hero gets prev / next chevrons plus a dot indicator
   when the current snapshot has more than one slide.
   Operator-accepted stories stay single — the carousel
   collapses when there's only one to show.

5. "Ich fände es schöner wenn rechts und links im Store
   die Abstände konsistent sind, das Reload-Symbol rechts
   ist zu weit rechts und Store links auch nicht bündig."
   AppBar now has `titleSpacing: FaiSpace.xl` so the
   title's left edge sits flush with the body's left
   padding (24 dp), and the trailing `SizedBox` after the
   reload icon shrunk so the icon's outer edge meets the
   right edge of the rightmost grid card.

6. "Oben der Titel zeigt fai_studio an, das sollte F∆I
   Studio sein." The OS window title was the
   pubspec-derived "fai_studio". Macos/Linux/Windows
   runners now hard-code "F∆I Studio" (with the U+2206
   triangle escape so the C++ source stays ASCII). macOS
   bundle name and display name lifted out of the
   PRODUCT_NAME variable for the same reason.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
2026-05-08 14:48:33 +02:00

113 lines
3.6 KiB
Dart

// 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<FaiErrorBox> createState() => _FaiErrorBoxState();
}
class _FaiErrorBoxState extends State<FaiErrorBox> {
bool _justCopied = false;
Future<void> _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),
),
),
),
),
],
),
);
}
}