chain-studio-flow-editor/lib/src/widgets.dart
flemming-it 29294a2971 feat: sample flows follow the hub's wire flag; examples group + import action
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>
2026-08-28 00:08:51 +02:00

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),
),
),
);
}
}