The Example chip used to come from a client-side content-marker scan that had drifted from the hub's renamed sample header — no current sample matched it. The host now passes the hub-reported sample set (FlowSummary.sample) and the editor renders truth only: no host info, no chips. Samples collapse under one 'Examples (N)' group below the operator's own flows (expanded only when nothing else exists), and an optional onImportSamples action on the empty list is the deliberate way to pull the bundled examples into a sealed area's empty hub. Guard: flow_list_samples_test pins chips-from-host-only, the grouping, and the import round trip. Signed-off-by: flemming-it <sf@flemming.it>
105 lines
2.9 KiB
Dart
105 lines
2.9 KiB
Dart
/// Minimal stand-ins for the FaiEmptyState + ChainErrorBox
|
|
/// widgets Studio ships. The editor brings its own so the
|
|
/// package compiles standalone — host-styled variants are
|
|
/// a future plugin-API extension.
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'tokens.dart';
|
|
|
|
class FaiEmptyState extends StatelessWidget {
|
|
final IconData icon;
|
|
final String title;
|
|
final String? hint;
|
|
|
|
/// Optional action rendered under the hint (e.g. the empty flow
|
|
/// list's "import example flows" button).
|
|
final Widget? action;
|
|
|
|
const FaiEmptyState({
|
|
super.key,
|
|
required this.icon,
|
|
required this.title,
|
|
this.hint,
|
|
this.action,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(FaiSpace.xl),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Icon(icon, size: 40, color: theme.colorScheme.onSurfaceVariant),
|
|
const SizedBox(height: FaiSpace.md),
|
|
Text(
|
|
title,
|
|
style: theme.textTheme.titleMedium,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
if (hint != null) ...[
|
|
const SizedBox(height: FaiSpace.sm),
|
|
Text(
|
|
hint!,
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
if (action != null) ...[
|
|
const SizedBox(height: FaiSpace.md),
|
|
action!,
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ChainErrorBox extends StatelessWidget {
|
|
final Object? error;
|
|
final bool isError;
|
|
final double? maxHeight;
|
|
const ChainErrorBox({
|
|
super.key,
|
|
required this.error,
|
|
this.isError = true,
|
|
this.maxHeight,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final fg = isError ? theme.colorScheme.error : theme.colorScheme.onSurface;
|
|
return Container(
|
|
constraints: maxHeight == null
|
|
? null
|
|
: BoxConstraints(maxHeight: maxHeight!),
|
|
padding: const EdgeInsets.all(FaiSpace.md),
|
|
decoration: BoxDecoration(
|
|
color: isError
|
|
? theme.colorScheme.errorContainer.withValues(alpha: 0.4)
|
|
: theme.colorScheme.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
|
border: Border.all(
|
|
color: isError
|
|
? theme.colorScheme.error.withValues(alpha: 0.4)
|
|
: theme.colorScheme.outlineVariant,
|
|
),
|
|
),
|
|
child: SingleChildScrollView(
|
|
child: SelectableText(
|
|
error?.toString() ?? '',
|
|
style: TextStyle(fontFamily: 'monospace', fontSize: 12, color: fg),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|