feat: initial scaffold — swappable flow editor

Extracted from fai/studio's lib/pages/flow_editor.dart so the
editor can be swapped out independently of the host.

Public surface kept minimal — a single FlowEditorPage widget
with three named parameters (initialFlowName, locale, onRun).
The package brings its own design tokens, empty/error
widgets, l10n table; no host-internal types leak through.

Studio depends on this repo via pubspec.yaml git reference.
Forks point Studio at a different URL and rebuild.

See README.md for the swap recipe.

Signed-off-by: F∆I Platform <platform@flemming.ai>
This commit is contained in:
F∆I Platform 2026-05-30 14:33:03 +02:00
commit 51f9a1d2b1
8 changed files with 1128 additions and 0 deletions

103
lib/src/widgets.dart Normal file
View file

@ -0,0 +1,103 @@
/// Minimal stand-ins for the FaiEmptyState + FaiErrorBox
/// 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;
const FaiEmptyState({
super.key,
required this.icon,
required this.title,
this.hint,
});
@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,
),
],
],
),
),
);
}
}
class FaiErrorBox extends StatelessWidget {
final Object? error;
final bool isError;
final double? maxHeight;
const FaiErrorBox({
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,
),
),
),
);
}
}