From 3a7d0e74ad4efcda44966c82745de3dbdaedab35 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Sat, 9 May 2026 00:17:54 +0200 Subject: [PATCH] =?UTF-8?q?feat(studio):=20Welcome=20page=20=E2=80=94=20Ph?= =?UTF-8?q?ase=20A=20scaffolding=20(v0.37.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First slice of the new sidebar destination outlined in docs/landing-page-design.md. Static content only — embedded docs (Phase B) and the live getting-started checklist (Phase C) follow. Phase A: - New `WelcomePage` at `lib/pages/welcome.dart`, registered as sidebar slot 0 above Doctor. Default selectedIndex stays 0, so a fresh launch lands on Welcome. - Hero card: gradient backdrop matching the Today-Hero in the store, F∆I Platform headline, subtitle taken from CLAUDE.md ("deterministic workflow engine for AI-assisted document processing in regulated environments"). - Three-pillar row "Hub / Module / Flow" — operator-readable prose, not architecture-doc dense. Stacks to a column under 640 dp window width so card text never gets cropped. - Trust-posture deck — "Sandbox by default", "Tamper-evident audit log", "Air-gap ready". Same content that used to rotate as carousel slides in the store; reading them as a single deck with full prose works better than rotating through fragments. - AppBar follows the same `titleSpacing: FaiSpace.xl` alignment as the Store so the title sits flush with the body padding. - 13 new ARB keys for the page content (EN + DE). Smoke test now expects "Welcome" in the navigation rail; the existing `Doctor / Store / Flows / Audit / Approvals` expectations stay unchanged. Signed-off-by: flemming-it --- lib/l10n/app_de.arb | 17 ++ lib/l10n/app_en.arb | 17 ++ lib/l10n/app_localizations.dart | 102 +++++++++ lib/l10n/app_localizations_de.dart | 58 +++++ lib/l10n/app_localizations_en.dart | 58 +++++ lib/main.dart | 11 +- lib/pages/welcome.dart | 351 +++++++++++++++++++++++++++++ pubspec.yaml | 2 +- test/widget_test.dart | 3 + 9 files changed, 617 insertions(+), 2 deletions(-) create mode 100644 lib/pages/welcome.dart diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 21dca6f..c6a5e94 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -3,6 +3,23 @@ "appTitle": "F∆I Studio", + "navWelcome": "Willkommen", + "welcomeHeroTitle": "F∆I Platform", + "welcomeHeroSubtitle": "Deterministische Workflow-Engine für KI-gestützte Dokumentenverarbeitung in regulierten Umgebungen.", + "welcomePillarsHeader": "WIE ES ZUSAMMENPASST", + "welcomePillarHubTitle": "Hub", + "welcomePillarHubBody": "Ein Rust-Binary, das Module lädt und Flows ausführt. SQLite für den State. Kein Docker, kein Message-Broker, keine Hintergrunddienste, die gepflegt werden müssen.", + "welcomePillarModuleTitle": "Modul", + "welcomePillarModuleBody": "Eine kleine WebAssembly-Komponente, die eine Sache macht. Deklariert Netzwerk-Endpunkte, Dateien und Umgebungsvariablen, auf die sie zugreift; der Hub setzt die Liste durch.", + "welcomePillarFlowTitle": "Flow", + "welcomePillarFlowBody": "Eine YAML-Datei, die Module verkettet. Eingaben rein, Module laufen in der deklarierten Reihenfolge, Ausgaben raus — jeder Schritt in einem hash-verketteten Audit-Log festgehalten.", + "welcomeTrustHeader": "TRUST-POSTURE", + "welcomeTrustSandboxTitle": "Sandbox von Anfang an", + "welcomeTrustSandboxBody": "Jedes Modul bringt eine explizite Berechtigungsliste mit — Netzwerk-Endpunkte, Dateien, Umgebungsvariablen. Der Hub setzt sie durch; ohne Operator-Freigabe verlässt nichts die Sandbox.", + "welcomeTrustAuditTitle": "Manipulationssicheres Audit-Log", + "welcomeTrustAuditBody": "Flow-Läufe, Installationen, Deinstallationen, Freigabe-Entscheidungen — alles wird in ein hash-verkettetes Audit-Log geschrieben. Die Diagnose-Seite verifiziert die Kette bei jedem Laden komplett.", + "welcomeTrustAirgapTitle": "Air-Gap-tauglich", + "welcomeTrustAirgapBody": "Der gesamte Hub steckt in einem einzigen Binary für Linux, macOS und Windows. Sobald ein Modul installiert ist, läuft der Flow, der es nutzt, ohne weiteren Netzwerkzugriff — ideal für regulierte Umgebungen.", "navDoctor": "Diagnose", "navModules": "Module", "navStore": "Store", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4dfee2c..5aa0eb0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -4,6 +4,23 @@ "appTitle": "F∆I Studio", "@appTitle": { "description": "App-bar title in the OS task switcher." }, + "navWelcome": "Welcome", + "welcomeHeroTitle": "F∆I Platform", + "welcomeHeroSubtitle": "Deterministic workflow engine for AI-assisted document processing in regulated environments.", + "welcomePillarsHeader": "HOW IT FITS TOGETHER", + "welcomePillarHubTitle": "Hub", + "welcomePillarHubBody": "One Rust binary that loads modules and runs flows. SQLite for state. No Docker, no message broker, no background services to babysit.", + "welcomePillarModuleTitle": "Module", + "welcomePillarModuleBody": "A small WebAssembly component that does one thing. Declares the network endpoints, files, and environment variables it touches; the hub enforces the list.", + "welcomePillarFlowTitle": "Flow", + "welcomePillarFlowBody": "A YAML file that chains modules together. Inputs go in, modules run in declared order, outputs come out — every step recorded in a hash-chained audit log.", + "welcomeTrustHeader": "TRUST POSTURE", + "welcomeTrustSandboxTitle": "Sandbox by default", + "welcomeTrustSandboxBody": "Every module ships with an explicit permission list — network endpoints, files, environment variables. The hub enforces it; nothing reaches outside the sandbox without operator approval.", + "welcomeTrustAuditTitle": "Tamper-evident audit log", + "welcomeTrustAuditBody": "Flow runs, install / uninstall actions, approval decisions — all of them write into a hash-chained audit log. The Doctor page verifies the chain end-to-end on every load.", + "welcomeTrustAirgapTitle": "Air-gap ready", + "welcomeTrustAirgapBody": "The whole hub fits in a single binary that runs on Linux, macOS and Windows. Once a module is installed, the flow it powers runs without further network access — perfect for regulated environments.", "navDoctor": "Doctor", "navModules": "Modules", "navStore": "Store", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 2266e9a..b9df715 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -104,6 +104,108 @@ abstract class AppLocalizations { /// **'F∆I Studio'** String get appTitle; + /// No description provided for @navWelcome. + /// + /// In en, this message translates to: + /// **'Welcome'** + String get navWelcome; + + /// No description provided for @welcomeHeroTitle. + /// + /// In en, this message translates to: + /// **'F∆I Platform'** + String get welcomeHeroTitle; + + /// No description provided for @welcomeHeroSubtitle. + /// + /// In en, this message translates to: + /// **'Deterministic workflow engine for AI-assisted document processing in regulated environments.'** + String get welcomeHeroSubtitle; + + /// No description provided for @welcomePillarsHeader. + /// + /// In en, this message translates to: + /// **'HOW IT FITS TOGETHER'** + String get welcomePillarsHeader; + + /// No description provided for @welcomePillarHubTitle. + /// + /// In en, this message translates to: + /// **'Hub'** + String get welcomePillarHubTitle; + + /// No description provided for @welcomePillarHubBody. + /// + /// In en, this message translates to: + /// **'One Rust binary that loads modules and runs flows. SQLite for state. No Docker, no message broker, no background services to babysit.'** + String get welcomePillarHubBody; + + /// No description provided for @welcomePillarModuleTitle. + /// + /// In en, this message translates to: + /// **'Module'** + String get welcomePillarModuleTitle; + + /// No description provided for @welcomePillarModuleBody. + /// + /// In en, this message translates to: + /// **'A small WebAssembly component that does one thing. Declares the network endpoints, files, and environment variables it touches; the hub enforces the list.'** + String get welcomePillarModuleBody; + + /// No description provided for @welcomePillarFlowTitle. + /// + /// In en, this message translates to: + /// **'Flow'** + String get welcomePillarFlowTitle; + + /// No description provided for @welcomePillarFlowBody. + /// + /// In en, this message translates to: + /// **'A YAML file that chains modules together. Inputs go in, modules run in declared order, outputs come out — every step recorded in a hash-chained audit log.'** + String get welcomePillarFlowBody; + + /// No description provided for @welcomeTrustHeader. + /// + /// In en, this message translates to: + /// **'TRUST POSTURE'** + String get welcomeTrustHeader; + + /// No description provided for @welcomeTrustSandboxTitle. + /// + /// In en, this message translates to: + /// **'Sandbox by default'** + String get welcomeTrustSandboxTitle; + + /// No description provided for @welcomeTrustSandboxBody. + /// + /// In en, this message translates to: + /// **'Every module ships with an explicit permission list — network endpoints, files, environment variables. The hub enforces it; nothing reaches outside the sandbox without operator approval.'** + String get welcomeTrustSandboxBody; + + /// No description provided for @welcomeTrustAuditTitle. + /// + /// In en, this message translates to: + /// **'Tamper-evident audit log'** + String get welcomeTrustAuditTitle; + + /// No description provided for @welcomeTrustAuditBody. + /// + /// In en, this message translates to: + /// **'Flow runs, install / uninstall actions, approval decisions — all of them write into a hash-chained audit log. The Doctor page verifies the chain end-to-end on every load.'** + String get welcomeTrustAuditBody; + + /// No description provided for @welcomeTrustAirgapTitle. + /// + /// In en, this message translates to: + /// **'Air-gap ready'** + String get welcomeTrustAirgapTitle; + + /// No description provided for @welcomeTrustAirgapBody. + /// + /// In en, this message translates to: + /// **'The whole hub fits in a single binary that runs on Linux, macOS and Windows. Once a module is installed, the flow it powers runs without further network access — perfect for regulated environments.'** + String get welcomeTrustAirgapBody; + /// No description provided for @navDoctor. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 48d8ff4..4c4e0d0 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -11,6 +11,64 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appTitle => 'F∆I Studio'; + @override + String get navWelcome => 'Willkommen'; + + @override + String get welcomeHeroTitle => 'F∆I Platform'; + + @override + String get welcomeHeroSubtitle => + 'Deterministische Workflow-Engine für KI-gestützte Dokumentenverarbeitung in regulierten Umgebungen.'; + + @override + String get welcomePillarsHeader => 'WIE ES ZUSAMMENPASST'; + + @override + String get welcomePillarHubTitle => 'Hub'; + + @override + String get welcomePillarHubBody => + 'Ein Rust-Binary, das Module lädt und Flows ausführt. SQLite für den State. Kein Docker, kein Message-Broker, keine Hintergrunddienste, die gepflegt werden müssen.'; + + @override + String get welcomePillarModuleTitle => 'Modul'; + + @override + String get welcomePillarModuleBody => + 'Eine kleine WebAssembly-Komponente, die eine Sache macht. Deklariert Netzwerk-Endpunkte, Dateien und Umgebungsvariablen, auf die sie zugreift; der Hub setzt die Liste durch.'; + + @override + String get welcomePillarFlowTitle => 'Flow'; + + @override + String get welcomePillarFlowBody => + 'Eine YAML-Datei, die Module verkettet. Eingaben rein, Module laufen in der deklarierten Reihenfolge, Ausgaben raus — jeder Schritt in einem hash-verketteten Audit-Log festgehalten.'; + + @override + String get welcomeTrustHeader => 'TRUST-POSTURE'; + + @override + String get welcomeTrustSandboxTitle => 'Sandbox von Anfang an'; + + @override + String get welcomeTrustSandboxBody => + 'Jedes Modul bringt eine explizite Berechtigungsliste mit — Netzwerk-Endpunkte, Dateien, Umgebungsvariablen. Der Hub setzt sie durch; ohne Operator-Freigabe verlässt nichts die Sandbox.'; + + @override + String get welcomeTrustAuditTitle => 'Manipulationssicheres Audit-Log'; + + @override + String get welcomeTrustAuditBody => + 'Flow-Läufe, Installationen, Deinstallationen, Freigabe-Entscheidungen — alles wird in ein hash-verkettetes Audit-Log geschrieben. Die Diagnose-Seite verifiziert die Kette bei jedem Laden komplett.'; + + @override + String get welcomeTrustAirgapTitle => 'Air-Gap-tauglich'; + + @override + String get welcomeTrustAirgapBody => + 'Der gesamte Hub steckt in einem einzigen Binary für Linux, macOS und Windows. Sobald ein Modul installiert ist, läuft der Flow, der es nutzt, ohne weiteren Netzwerkzugriff — ideal für regulierte Umgebungen.'; + @override String get navDoctor => 'Diagnose'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index be45698..5683ff6 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -11,6 +11,64 @@ class AppLocalizationsEn extends AppLocalizations { @override String get appTitle => 'F∆I Studio'; + @override + String get navWelcome => 'Welcome'; + + @override + String get welcomeHeroTitle => 'F∆I Platform'; + + @override + String get welcomeHeroSubtitle => + 'Deterministic workflow engine for AI-assisted document processing in regulated environments.'; + + @override + String get welcomePillarsHeader => 'HOW IT FITS TOGETHER'; + + @override + String get welcomePillarHubTitle => 'Hub'; + + @override + String get welcomePillarHubBody => + 'One Rust binary that loads modules and runs flows. SQLite for state. No Docker, no message broker, no background services to babysit.'; + + @override + String get welcomePillarModuleTitle => 'Module'; + + @override + String get welcomePillarModuleBody => + 'A small WebAssembly component that does one thing. Declares the network endpoints, files, and environment variables it touches; the hub enforces the list.'; + + @override + String get welcomePillarFlowTitle => 'Flow'; + + @override + String get welcomePillarFlowBody => + 'A YAML file that chains modules together. Inputs go in, modules run in declared order, outputs come out — every step recorded in a hash-chained audit log.'; + + @override + String get welcomeTrustHeader => 'TRUST POSTURE'; + + @override + String get welcomeTrustSandboxTitle => 'Sandbox by default'; + + @override + String get welcomeTrustSandboxBody => + 'Every module ships with an explicit permission list — network endpoints, files, environment variables. The hub enforces it; nothing reaches outside the sandbox without operator approval.'; + + @override + String get welcomeTrustAuditTitle => 'Tamper-evident audit log'; + + @override + String get welcomeTrustAuditBody => + 'Flow runs, install / uninstall actions, approval decisions — all of them write into a hash-chained audit log. The Doctor page verifies the chain end-to-end on every load.'; + + @override + String get welcomeTrustAirgapTitle => 'Air-gap ready'; + + @override + String get welcomeTrustAirgapBody => + 'The whole hub fits in a single binary that runs on Linux, macOS and Windows. Once a module is installed, the flow it powers runs without further network access — perfect for regulated environments.'; + @override String get navDoctor => 'Doctor'; diff --git a/lib/main.dart b/lib/main.dart index edb7a09..07e03d3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,6 +17,7 @@ import 'pages/audit.dart'; import 'pages/doctor.dart'; import 'pages/flows.dart'; import 'pages/store.dart'; +import 'pages/welcome.dart'; import 'theme/theme.dart'; import 'theme/tokens.dart'; import 'widgets/fai_search_palette.dart'; @@ -25,7 +26,7 @@ import 'widgets/widgets.dart'; /// Studio's own build version. Bump on every UI commit so the /// running app self-identifies — visible in the sidebar header /// and quick-glance proof that you're seeing the current build. -const String kStudioVersion = '0.36.0'; +const String kStudioVersion = '0.37.0'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -139,6 +140,12 @@ class _StudioShellState extends State { Timer? _healthPoll; static const _pages = <_NavPage>[ + _NavPage( + id: 'welcome', + icon: Icons.waving_hand_outlined, + selectedIcon: Icons.waving_hand, + page: WelcomePage(), + ), _NavPage( id: 'doctor', icon: Icons.health_and_safety_outlined, @@ -752,6 +759,8 @@ class _NavPage { final l = AppLocalizations.of(context); if (l == null) return id; switch (id) { + case 'welcome': + return l.navWelcome; case 'doctor': return l.navDoctor; case 'modules': diff --git a/lib/pages/welcome.dart b/lib/pages/welcome.dart new file mode 100644 index 0000000..8b17895 --- /dev/null +++ b/lib/pages/welcome.dart @@ -0,0 +1,351 @@ +// Welcome — first surface a new operator sees, default sidebar +// destination on launch. Pure introduction: what F∆I is, the +// three concepts, the trust posture. No live data, no +// federation chrome — that's the Store's role. +// +// See docs/landing-page-design.md for the multi-phase plan; +// this file is Phase A (scaffolding + static content). Phase B +// adds an embedded markdown reader; Phase C adds a live +// getting-started checklist. + +import 'package:flutter/material.dart'; + +import '../l10n/app_localizations.dart'; +import '../theme/tokens.dart'; + +class WelcomePage extends StatelessWidget { + const WelcomePage({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; + return Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, + appBar: AppBar( + titleSpacing: FaiSpace.xl, + title: Text(l.navWelcome), + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.fromLTRB( + FaiSpace.xl, + FaiSpace.lg, + FaiSpace.xl, + FaiSpace.xxl, + ), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 960), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + _Hero(), + SizedBox(height: FaiSpace.xxl), + _SectionLabel(textKey: _SectionLabelKey.pillars), + SizedBox(height: FaiSpace.md), + _PillarRow(), + SizedBox(height: FaiSpace.xxl), + _SectionLabel(textKey: _SectionLabelKey.trust), + SizedBox(height: FaiSpace.md), + _TrustPosture(), + ], + ), + ), + ), + ), + ); + } +} + +/// Top hero — gradient backdrop matching the Today-Hero in the +/// store so the visual language carries between pages. Title + +/// subtitle, no CTAs. +class _Hero extends StatelessWidget { + const _Hero(); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; + return Container( + width: double.infinity, + padding: const EdgeInsets.all(FaiSpace.xxl), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(FaiRadius.md), + gradient: LinearGradient( + colors: [ + theme.colorScheme.primary.withValues(alpha: 0.18), + theme.colorScheme.primary.withValues(alpha: 0.04), + theme.colorScheme.surfaceContainer.withValues(alpha: 0.0), + ], + stops: const [0.0, 0.55, 1.0], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + border: Border.all( + color: theme.colorScheme.primary.withValues(alpha: 0.25), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(FaiSpace.md), + decoration: BoxDecoration( + color: theme.colorScheme.primary.withValues(alpha: 0.14), + shape: BoxShape.circle, + ), + child: Icon( + Icons.waving_hand_outlined, + size: 28, + color: theme.colorScheme.primary, + ), + ), + const SizedBox(width: FaiSpace.lg), + Expanded( + child: Text( + l.welcomeHeroTitle, + style: theme.textTheme.displaySmall?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + const SizedBox(height: FaiSpace.lg), + Text( + l.welcomeHeroSubtitle, + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + height: 1.45, + ), + ), + ], + ), + ); + } +} + +enum _SectionLabelKey { pillars, trust } + +class _SectionLabel extends StatelessWidget { + final _SectionLabelKey textKey; + const _SectionLabel({required this.textKey}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; + final text = switch (textKey) { + _SectionLabelKey.pillars => l.welcomePillarsHeader, + _SectionLabelKey.trust => l.welcomeTrustHeader, + }; + return Text( + text, + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + letterSpacing: 0.6, + fontSize: 10, + ), + ); + } +} + +/// "Hub. Module. Flow." three-card row. Stacks on narrow +/// windows so card text never gets cropped. +class _PillarRow extends StatelessWidget { + const _PillarRow(); + + @override + Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; + final pillars = <(IconData, String, String)>[ + ( + Icons.hub_outlined, + l.welcomePillarHubTitle, + l.welcomePillarHubBody, + ), + ( + Icons.extension_outlined, + l.welcomePillarModuleTitle, + l.welcomePillarModuleBody, + ), + ( + Icons.account_tree_outlined, + l.welcomePillarFlowTitle, + l.welcomePillarFlowBody, + ), + ]; + return LayoutBuilder( + builder: (context, constraints) { + // ~640 dp is the comfortable threshold for three cards + // side by side; below that we stack so each card + // gets its full text width. + final stacked = constraints.maxWidth < 640; + if (stacked) { + return Column( + children: [ + for (final p in pillars) ...[ + _Pillar(icon: p.$1, title: p.$2, body: p.$3), + const SizedBox(height: FaiSpace.md), + ], + ], + ); + } + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + for (var i = 0; i < pillars.length; i++) ...[ + if (i > 0) const SizedBox(width: FaiSpace.md), + Expanded( + child: _Pillar( + icon: pillars[i].$1, + title: pillars[i].$2, + body: pillars[i].$3, + ), + ), + ], + ], + ); + }, + ); + } +} + +class _Pillar extends StatelessWidget { + final IconData icon; + final String title; + final String body; + + const _Pillar({ + required this.icon, + required this.title, + required this.body, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + padding: const EdgeInsets.all(FaiSpace.lg), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(FaiRadius.md), + border: Border.all(color: theme.colorScheme.outlineVariant), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 28, color: theme.colorScheme.primary), + const SizedBox(height: FaiSpace.md), + Text( + title, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: FaiSpace.sm), + Text( + body, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + height: 1.45, + ), + ), + ], + ), + ); + } +} + +/// Three trust-posture rows: Sandbox / Audit / Air-gap. Used to +/// live as carousel slides in the store; reading them here as +/// a single deck reads better than rotating through them. +class _TrustPosture extends StatelessWidget { + const _TrustPosture(); + + @override + Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; + final rows = <(IconData, String, String)>[ + ( + Icons.shield_outlined, + l.welcomeTrustSandboxTitle, + l.welcomeTrustSandboxBody, + ), + ( + Icons.verified_outlined, + l.welcomeTrustAuditTitle, + l.welcomeTrustAuditBody, + ), + ( + Icons.rocket_launch_outlined, + l.welcomeTrustAirgapTitle, + l.welcomeTrustAirgapBody, + ), + ]; + return Column( + children: [ + for (var i = 0; i < rows.length; i++) ...[ + if (i > 0) const SizedBox(height: FaiSpace.sm), + _TrustRow(icon: rows[i].$1, title: rows[i].$2, body: rows[i].$3), + ], + ], + ); + } +} + +class _TrustRow extends StatelessWidget { + final IconData icon; + final String title; + final String body; + + const _TrustRow({ + required this.icon, + required this.title, + required this.body, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + padding: const EdgeInsets.all(FaiSpace.lg), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(FaiRadius.md), + border: Border.all(color: theme.colorScheme.outlineVariant), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 22, color: theme.colorScheme.primary), + const SizedBox(width: FaiSpace.lg), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + body, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + height: 1.45, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index a23b5a3..829e3d8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fai_studio description: "F∆I Studio — desktop GUI for the F∆I hub" publish_to: 'none' -version: 0.36.0 +version: 0.37.0 environment: sdk: ^3.11.0-200.1.beta diff --git a/test/widget_test.dart b/test/widget_test.dart index 904b079..b47ad80 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -16,6 +16,9 @@ void main() { ); expect(find.text('F∆I Studio'), findsOneWidget); + // Welcome tab added in v0.37.0 — first surface a new + // operator sees. + expect(find.text('Welcome'), findsWidgets); expect(find.text('Doctor'), findsWidgets); // Modules tab dropped in v0.35.0 — info now lives inside // the Store detail sheet.