// FaiModuleSheet — modal bottom-sheet showing detailed module // info: full capability list, declared permissions, on-disk // directory. Opened by tapping a module card on the Modules // page. import 'package:flutter/material.dart'; import '../data/hub.dart'; import '../theme/theme.dart'; import '../theme/tokens.dart'; import 'fai_pill.dart'; class FaiModuleSheet extends StatefulWidget { final String moduleName; const FaiModuleSheet({super.key, required this.moduleName}); /// Convenience launcher used from the Modules list. static Future show(BuildContext context, String name) { return showModalBottomSheet( context: context, backgroundColor: Theme.of(context).colorScheme.surfaceContainer, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(FaiRadius.md), ), ), builder: (_) => FaiModuleSheet(moduleName: name), ); } @override State createState() => _FaiModuleSheetState(); } class _FaiModuleSheetState extends State { late final Future _future; @override void initState() { super.initState(); _future = HubService.instance.moduleInfo(widget.moduleName); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final maxHeight = MediaQuery.of(context).size.height * 0.7; return ConstrainedBox( constraints: BoxConstraints(maxHeight: maxHeight), child: FutureBuilder( future: _future, builder: (context, snapshot) { return Column( mainAxisSize: MainAxisSize.min, children: [ _Handle(), if (snapshot.connectionState == ConnectionState.waiting) const Padding( padding: EdgeInsets.all(FaiSpace.xxxl), child: CircularProgressIndicator(), ) else if (snapshot.hasError) Padding( padding: const EdgeInsets.all(FaiSpace.xl), child: Text( 'Failed to load: ${snapshot.error}', style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.error, ), ), ) else Flexible(child: _Body(detail: snapshot.data!)), ], ); }, ), ); } } class _Handle extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); return Padding( padding: const EdgeInsets.symmetric(vertical: FaiSpace.sm), child: Container( width: 40, height: 4, decoration: BoxDecoration( color: theme.colorScheme.outlineVariant, borderRadius: BorderRadius.circular(2), ), ), ); } } class _Body extends StatelessWidget { final ModuleDetail detail; const _Body({required this.detail}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return ListView( padding: const EdgeInsets.fromLTRB( FaiSpace.xxl, FaiSpace.md, FaiSpace.xxl, FaiSpace.xxl, ), shrinkWrap: true, children: [ Row( children: [ Text( detail.name, style: theme.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(width: FaiSpace.md), FaiPill( label: 'v${detail.version}', tone: FaiPillTone.accent, monospace: true, ), ], ), const SizedBox(height: FaiSpace.xs), SelectableText( detail.directory, style: FaiTheme.mono( size: 11, color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: FaiSpace.xl), _SectionHeader('Capabilities'), const SizedBox(height: FaiSpace.sm), Wrap( spacing: FaiSpace.xs, runSpacing: FaiSpace.xs, children: detail.capabilities .map( (c) => FaiPill( label: c, tone: FaiPillTone.accent, monospace: true, ), ) .toList(), ), const SizedBox(height: FaiSpace.xl), _SectionHeader('Declared permissions'), const SizedBox(height: FaiSpace.sm), if (detail.permissions.isEmpty) Text( '(none — pure-computation module)', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, fontStyle: FontStyle.italic, ), ) else Column( crossAxisAlignment: CrossAxisAlignment.start, children: detail.permissions .map( (p) => Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row( children: [ Icon( _iconFor(p), size: 14, color: theme.colorScheme.onSurfaceVariant, ), const SizedBox(width: FaiSpace.sm), SelectableText( p, style: FaiTheme.mono( size: 12, color: theme.colorScheme.onSurface, ), ), ], ), ), ) .toList(), ), ], ); } IconData _iconFor(String permission) { if (permission.startsWith('net:')) return Icons.public; if (permission.startsWith('fs.read:')) return Icons.folder_open; if (permission.startsWith('fs.write:')) return Icons.edit_note; if (permission.startsWith('env:')) return Icons.terminal; if (permission.startsWith('hub:')) return Icons.shield_outlined; return Icons.lock_outline; } } class _SectionHeader extends StatelessWidget { final String text; const _SectionHeader(this.text); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Text( text.toUpperCase(), style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, letterSpacing: 0.6, fontSize: 10, ), ); } }