// 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 'package:flutter/services.dart' show rootBundle; import 'package:flutter_markdown/flutter_markdown.dart'; import '../data/system_actions.dart'; import '../l10n/app_localizations.dart'; import '../theme/theme.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(), SizedBox(height: FaiSpace.xxl), _SectionLabel(textKey: _SectionLabelKey.docs), SizedBox(height: FaiSpace.md), _DocsRow(), ], ), ), ), ), ); } } /// 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, docs } 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, _SectionLabelKey.docs => l.welcomeDocsHeader, }; 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, ), ), ], ), ), ], ), ); } } /// Doc-card entries — each links to a bundled markdown file /// under `assets/docs/`. The slug is the filename stem; the /// loader picks `.md` or `_de.md` per locale. class _DocEntry { final String slug; final IconData icon; final String Function(AppLocalizations) title; final String Function(AppLocalizations) blurb; const _DocEntry({ required this.slug, required this.icon, required this.title, required this.blurb, }); } final List<_DocEntry> _kDocs = <_DocEntry>[ _DocEntry( slug: 'architecture', icon: Icons.account_tree_outlined, title: (l) => l.welcomeDocArchitectureTitle, blurb: (l) => l.welcomeDocArchitectureBlurb, ), _DocEntry( slug: 'security', icon: Icons.shield_outlined, title: (l) => l.welcomeDocSecurityTitle, blurb: (l) => l.welcomeDocSecurityBlurb, ), _DocEntry( slug: 'audit', icon: Icons.verified_outlined, title: (l) => l.welcomeDocAuditTitle, blurb: (l) => l.welcomeDocAuditBlurb, ), _DocEntry( slug: 'flows', icon: Icons.alt_route_outlined, title: (l) => l.welcomeDocFlowsTitle, blurb: (l) => l.welcomeDocFlowsBlurb, ), ]; /// Two-by-two doc grid on wide windows, single column on /// narrow. Each card opens `_DocReaderSheet` for its slug. class _DocsRow extends StatelessWidget { const _DocsRow(); @override Widget build(BuildContext context) { final l = AppLocalizations.of(context)!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l.welcomeDocsBody, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), const SizedBox(height: FaiSpace.md), LayoutBuilder( builder: (context, constraints) { final twoCols = constraints.maxWidth >= 640; final cardWidth = twoCols ? (constraints.maxWidth - FaiSpace.md) / 2 : double.infinity; return Wrap( spacing: FaiSpace.md, runSpacing: FaiSpace.md, children: [ for (final d in _kDocs) SizedBox( width: cardWidth, child: _DocCard(entry: d), ), ], ); }, ), ], ); } } class _DocCard extends StatelessWidget { final _DocEntry entry; const _DocCard({required this.entry}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final l = AppLocalizations.of(context)!; return Material( color: theme.colorScheme.surfaceContainer, borderRadius: BorderRadius.circular(FaiRadius.md), child: InkWell( onTap: () => _DocReaderSheet.show(context, entry), borderRadius: BorderRadius.circular(FaiRadius.md), child: Container( padding: const EdgeInsets.all(FaiSpace.lg), decoration: BoxDecoration( borderRadius: BorderRadius.circular(FaiRadius.md), border: Border.all(color: theme.colorScheme.outlineVariant), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(entry.icon, size: 22, color: theme.colorScheme.primary), const SizedBox(width: FaiSpace.lg), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( entry.title(l), style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 4), Text( entry.blurb(l), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, height: 1.4, ), ), ], ), ), Icon( Icons.chevron_right, size: 18, color: theme.colorScheme.onSurfaceVariant, ), ], ), ), ), ); } } /// Modal bottom-sheet that loads the markdown for a doc entry /// from `assets/docs/[_de].md` and renders it inline via /// flutter_markdown. No browser, no network — KRITIS-friendly. class _DocReaderSheet extends StatefulWidget { final _DocEntry entry; const _DocReaderSheet({required this.entry}); static Future show(BuildContext context, _DocEntry entry) { return showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Theme.of(context).colorScheme.surfaceContainer, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(FaiRadius.md)), ), builder: (_) => _DocReaderSheet(entry: entry), ); } @override State<_DocReaderSheet> createState() => _DocReaderSheetState(); } class _DocReaderSheetState extends State<_DocReaderSheet> { late final Future _content; @override void initState() { super.initState(); _content = _load(); } Future _load() async { final locale = Localizations.localeOf(context).languageCode; final localised = 'assets/docs/${widget.entry.slug}_$locale.md'; final fallback = 'assets/docs/${widget.entry.slug}.md'; try { return await rootBundle.loadString(localised); } catch (_) { return rootBundle.loadString(fallback); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final l = AppLocalizations.of(context)!; final maxHeight = MediaQuery.of(context).size.height * 0.85; return ConstrainedBox( constraints: BoxConstraints(maxHeight: maxHeight), child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.symmetric(vertical: FaiSpace.sm), child: Container( width: 40, height: 4, decoration: BoxDecoration( color: theme.colorScheme.outlineVariant, borderRadius: BorderRadius.circular(2), ), ), ), Padding( padding: const EdgeInsets.fromLTRB( FaiSpace.xxl, FaiSpace.sm, FaiSpace.lg, FaiSpace.sm, ), child: Row( children: [ Icon(widget.entry.icon, size: 20, color: theme.colorScheme.primary), const SizedBox(width: FaiSpace.sm), Expanded( child: Text( widget.entry.title(l), style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), ), IconButton( icon: const Icon(Icons.close, size: 18), visualDensity: VisualDensity.compact, tooltip: l.welcomeDocClose, onPressed: () => Navigator.pop(context), ), ], ), ), const Divider(height: 1), Flexible( child: FutureBuilder( future: _content, builder: (context, snap) { if (snap.connectionState == ConnectionState.waiting) { return const Padding( padding: EdgeInsets.all(FaiSpace.xxl), child: Center(child: CircularProgressIndicator()), ); } if (snap.hasError) { return Padding( padding: const EdgeInsets.all(FaiSpace.xxl), child: Text( l.welcomeDocFailedToLoad(snap.error.toString()), style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.error, ), ), ); } return Markdown( data: snap.data ?? '', padding: const EdgeInsets.all(FaiSpace.xxl), selectable: true, onTapLink: (text, href, title) async { if (href != null && href.isNotEmpty) { await SystemActions.openInOs(href); } }, styleSheet: MarkdownStyleSheet.fromTheme(theme).copyWith( p: theme.textTheme.bodyMedium?.copyWith(height: 1.5), code: FaiTheme.mono( size: 12, color: theme.colorScheme.onSurface, ), codeblockDecoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(FaiRadius.sm), ), ), ); }, ), ), ], ), ); } }