feat(ui): warm-minimalism redesign — design tokens + 6 primitives + animated ∆
Replaces the Material-default look with a deliberate visual
language. Single accent (sky-cyan), Inter + JetBrains Mono via
google_fonts, dense rows over puffy cards, micro-interactions
under 250ms. Dark-first, light reaches parity.
New foundation under lib/theme/:
- tokens.dart FaiColors / FaiSpace / FaiRadius / FaiMotion
- theme.dart ColorScheme + typography + component themes for
light and dark, plus FaiTheme.mono helper for
technical strings (IDs, paths, capability refs).
Six primitives under lib/widgets/:
- FaiCard flat card, optional accent stripe (top or
left). No shadows.
- FaiPill small inline label with five tones
(neutral / accent / success / warning /
danger), optional mono and leading icon.
- FaiStatusDot breathing dot, used as a "live" indicator.
- FaiDataRow Linear-style dense row for the audit
stream — hover-elevation, mono leading,
coloured leading stripe.
- FaiEmptyState gracious icon + title + hint + action,
replaces "(no data)" everywhere.
- FaiDeltaMark the ∆ signature element. Three modes —
idle (still), live (gentle pulse), busy
(slow rotation). Drawn from primitives,
not a font glyph. Lives in the sidebar
header so the brand is always visible.
Page-level changes:
- main.dart custom 220px sidebar replaces the Material
NavigationRail. ∆ on top, hub-connection
pill below it, hover-animated destinations,
page transitions are 200ms slide+fade.
- modules.dart FaiCard rows with capability pills.
- audit.dart FaiDataRow stream, segmented filter chips,
live status bar with hash-chain badge.
- approvals.dart FaiCard with accent-top stripe, structured
action footer, toast on approve/reject,
themed reject-reason dialog.
google_fonts added as dep. flutter analyze clean. flutter test
2/2. flutter build macos --debug succeeds.
Bumps fai_studio 0.2.0 -> 0.3.0.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
127c497f73
commit
c94504247f
23 changed files with 1951 additions and 468 deletions
325
lib/main.dart
325
lib/main.dart
|
|
@ -1,14 +1,19 @@
|
||||||
// F∆I Studio — desktop GUI for the F∆I hub.
|
// F∆I Studio — desktop GUI for the F∆I hub.
|
||||||
//
|
//
|
||||||
// MVP scope: three pages (Modules, Audit, Approvals) with mock
|
// Visual language: warm minimalism / deep tech with soul.
|
||||||
// data, sharing one navigation shell. Live gRPC connection
|
// Tokens in lib/theme/, primitives in lib/widgets/.
|
||||||
// arrives in the next iteration via fai_dart_sdk.
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'data/hub.dart';
|
||||||
import 'pages/approvals.dart';
|
import 'pages/approvals.dart';
|
||||||
import 'pages/audit.dart';
|
import 'pages/audit.dart';
|
||||||
import 'pages/modules.dart';
|
import 'pages/modules.dart';
|
||||||
|
import 'theme/theme.dart';
|
||||||
|
import 'theme/tokens.dart';
|
||||||
|
import 'widgets/widgets.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const StudioApp());
|
runApp(const StudioApp());
|
||||||
|
|
@ -21,20 +26,10 @@ class StudioApp extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'F∆I Studio',
|
title: 'F∆I Studio',
|
||||||
theme: ThemeData(
|
debugShowCheckedModeBanner: false,
|
||||||
useMaterial3: true,
|
theme: FaiTheme.light(),
|
||||||
colorScheme: ColorScheme.fromSeed(
|
darkTheme: FaiTheme.dark(),
|
||||||
seedColor: const Color(0xFF1E3A8A),
|
themeMode: ThemeMode.system,
|
||||||
brightness: Brightness.light,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
darkTheme: ThemeData(
|
|
||||||
useMaterial3: true,
|
|
||||||
colorScheme: ColorScheme.fromSeed(
|
|
||||||
seedColor: const Color(0xFF1E3A8A),
|
|
||||||
brightness: Brightness.dark,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
home: const StudioShell(),
|
home: const StudioShell(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -49,6 +44,8 @@ class StudioShell extends StatefulWidget {
|
||||||
|
|
||||||
class _StudioShellState extends State<StudioShell> {
|
class _StudioShellState extends State<StudioShell> {
|
||||||
int _selectedIndex = 0;
|
int _selectedIndex = 0;
|
||||||
|
bool? _connected; // null = unknown, polled on a timer
|
||||||
|
Timer? _healthPoll;
|
||||||
|
|
||||||
static const _pages = <_NavPage>[
|
static const _pages = <_NavPage>[
|
||||||
_NavPage(
|
_NavPage(
|
||||||
|
|
@ -71,57 +68,281 @@ class _StudioShellState extends State<StudioShell> {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_checkHealth();
|
||||||
|
_healthPoll = Timer.periodic(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
(_) => _checkHealth(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkHealth() async {
|
||||||
|
final ok = await HubService.instance.healthy();
|
||||||
|
if (!mounted) return;
|
||||||
|
if (_connected != ok) setState(() => _connected = ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_healthPoll?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Row(
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
NavigationRail(
|
_Sidebar(
|
||||||
extended: true,
|
|
||||||
minExtendedWidth: 180,
|
|
||||||
selectedIndex: _selectedIndex,
|
selectedIndex: _selectedIndex,
|
||||||
onDestinationSelected: (i) => setState(() => _selectedIndex = i),
|
onSelect: (i) => setState(() => _selectedIndex = i),
|
||||||
leading: Padding(
|
pages: _pages,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
connected: _connected,
|
||||||
child: Column(
|
endpointLabel: HubService.instance.endpointLabel,
|
||||||
children: [
|
),
|
||||||
Icon(Icons.hub_outlined, size: 32, color: theme.colorScheme.primary),
|
Container(
|
||||||
const SizedBox(height: 8),
|
width: 1,
|
||||||
Text(
|
color: theme.colorScheme.outlineVariant,
|
||||||
'F∆I Studio',
|
),
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
Expanded(
|
||||||
color: theme.colorScheme.primary,
|
child: AnimatedSwitcher(
|
||||||
fontWeight: FontWeight.w600,
|
duration: FaiMotion.base,
|
||||||
),
|
switchInCurve: FaiMotion.easing,
|
||||||
),
|
switchOutCurve: FaiMotion.easing,
|
||||||
Text(
|
transitionBuilder: (child, anim) => FadeTransition(
|
||||||
'connected: localhost:50051',
|
opacity: anim,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
child: SlideTransition(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
position: Tween<Offset>(
|
||||||
),
|
begin: const Offset(0, 0.02),
|
||||||
),
|
end: Offset.zero,
|
||||||
],
|
).animate(anim),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: KeyedSubtree(
|
||||||
|
key: ValueKey(_selectedIndex),
|
||||||
|
child: _pages[_selectedIndex].page,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
destinations: _pages
|
|
||||||
.map(
|
|
||||||
(p) => NavigationRailDestination(
|
|
||||||
icon: Icon(p.icon),
|
|
||||||
selectedIcon: Icon(p.selectedIcon),
|
|
||||||
label: Text(p.label),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
),
|
||||||
const VerticalDivider(width: 1),
|
|
||||||
Expanded(child: _pages[_selectedIndex].page),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _Sidebar extends StatelessWidget {
|
||||||
|
final int selectedIndex;
|
||||||
|
final ValueChanged<int> onSelect;
|
||||||
|
final List<_NavPage> pages;
|
||||||
|
final bool? connected;
|
||||||
|
final String endpointLabel;
|
||||||
|
|
||||||
|
const _Sidebar({
|
||||||
|
required this.selectedIndex,
|
||||||
|
required this.onSelect,
|
||||||
|
required this.pages,
|
||||||
|
required this.connected,
|
||||||
|
required this.endpointLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final mode = connected == true ? FaiDeltaMode.live : FaiDeltaMode.idle;
|
||||||
|
return Container(
|
||||||
|
width: 220,
|
||||||
|
color: theme.colorScheme.surfaceContainerLow,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: FaiSpace.xl),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Brand mark.
|
||||||
|
FaiDeltaMark(color: theme.colorScheme.primary, mode: mode),
|
||||||
|
const SizedBox(height: FaiSpace.md),
|
||||||
|
Text(
|
||||||
|
'F∆I Studio',
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: FaiSpace.xs),
|
||||||
|
// Connection pill.
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: FaiSpace.lg),
|
||||||
|
child: _ConnectionPill(
|
||||||
|
connected: connected,
|
||||||
|
label: endpointLabel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: FaiSpace.xxl),
|
||||||
|
// Destinations.
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: FaiSpace.md),
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < pages.length; i++)
|
||||||
|
_SidebarItem(
|
||||||
|
page: pages[i],
|
||||||
|
selected: i == selectedIndex,
|
||||||
|
onTap: () => onSelect(i),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Footer hint.
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: FaiSpace.lg),
|
||||||
|
child: Text(
|
||||||
|
'platform v0.10.42',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConnectionPill extends StatelessWidget {
|
||||||
|
final bool? connected;
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
const _ConnectionPill({required this.connected, required this.label});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final dotColor = connected == true
|
||||||
|
? FaiColors.success
|
||||||
|
: connected == false
|
||||||
|
? FaiColors.danger
|
||||||
|
: FaiColors.muted;
|
||||||
|
final caption = connected == true
|
||||||
|
? 'connected'
|
||||||
|
: connected == false
|
||||||
|
? 'unreachable'
|
||||||
|
: 'connecting…';
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: FaiSpace.sm,
|
||||||
|
vertical: 6,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||||
|
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
FaiStatusDot(color: dotColor, pulsing: connected == true),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
caption,
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: dotColor == FaiColors.danger
|
||||||
|
? theme.colorScheme.error
|
||||||
|
: theme.colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: FaiTheme.mono(
|
||||||
|
size: 10,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SidebarItem extends StatefulWidget {
|
||||||
|
final _NavPage page;
|
||||||
|
final bool selected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _SidebarItem({
|
||||||
|
required this.page,
|
||||||
|
required this.selected,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SidebarItem> createState() => _SidebarItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SidebarItemState extends State<_SidebarItem> {
|
||||||
|
bool _hovered = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final color = widget.selected
|
||||||
|
? theme.colorScheme.primary
|
||||||
|
: _hovered
|
||||||
|
? theme.colorScheme.onSurface
|
||||||
|
: theme.colorScheme.onSurfaceVariant;
|
||||||
|
final bg = widget.selected
|
||||||
|
? theme.colorScheme.primary.withValues(alpha: 0.08)
|
||||||
|
: _hovered
|
||||||
|
? theme.colorScheme.surfaceContainerHigh
|
||||||
|
: Colors.transparent;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
|
child: MouseRegion(
|
||||||
|
onEnter: (_) => setState(() => _hovered = true),
|
||||||
|
onExit: (_) => setState(() => _hovered = false),
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: widget.onTap,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: FaiMotion.fast,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: FaiSpace.md,
|
||||||
|
vertical: FaiSpace.md,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bg,
|
||||||
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
widget.selected ? widget.page.selectedIcon : widget.page.icon,
|
||||||
|
size: 18,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
const SizedBox(width: FaiSpace.md),
|
||||||
|
Text(
|
||||||
|
widget.page.label,
|
||||||
|
style: theme.textTheme.labelMedium?.copyWith(color: color),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _NavPage {
|
class _NavPage {
|
||||||
final String label;
|
final String label;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../data/hub.dart';
|
import '../data/hub.dart';
|
||||||
|
import '../theme/theme.dart';
|
||||||
|
import '../theme/tokens.dart';
|
||||||
|
import '../widgets/widgets.dart';
|
||||||
|
|
||||||
class ApprovalsPage extends StatefulWidget {
|
class ApprovalsPage extends StatefulWidget {
|
||||||
const ApprovalsPage({super.key});
|
const ApprovalsPage({super.key});
|
||||||
|
|
@ -28,9 +31,10 @@ class _ApprovalsPageState extends State<ApprovalsPage> {
|
||||||
Future<void> _approve(PendingApproval a) async {
|
Future<void> _approve(PendingApproval a) async {
|
||||||
try {
|
try {
|
||||||
await HubService.instance.approve(a.id, _reviewer);
|
await HubService.instance.approve(a.id, _reviewer);
|
||||||
|
_toast('approved · ${a.flowName} › ${a.stepId}');
|
||||||
_refresh();
|
_refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_showError('approve failed: $e');
|
_toast('approve failed: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,13 +43,14 @@ class _ApprovalsPageState extends State<ApprovalsPage> {
|
||||||
if (reason == null || reason.isEmpty) return;
|
if (reason == null || reason.isEmpty) return;
|
||||||
try {
|
try {
|
||||||
await HubService.instance.reject(a.id, _reviewer, reason);
|
await HubService.instance.reject(a.id, _reviewer, reason);
|
||||||
|
_toast('rejected · ${a.flowName} › ${a.stepId}');
|
||||||
_refresh();
|
_refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_showError('reject failed: $e');
|
_toast('reject failed: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showError(String msg) {
|
void _toast(String msg) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||||
}
|
}
|
||||||
|
|
@ -61,6 +66,7 @@ class _ApprovalsPageState extends State<ApprovalsPage> {
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Reason (recorded in audit log)',
|
labelText: 'Reason (recorded in audit log)',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
|
@ -70,6 +76,10 @@ class _ApprovalsPageState extends State<ApprovalsPage> {
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => Navigator.pop(ctx, controller.text),
|
onPressed: () => Navigator.pop(ctx, controller.text),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: Theme.of(ctx).colorScheme.error,
|
||||||
|
foregroundColor: Theme.of(ctx).colorScheme.onError,
|
||||||
|
),
|
||||||
child: const Text('Reject'),
|
child: const Text('Reject'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -81,15 +91,16 @@ class _ApprovalsPageState extends State<ApprovalsPage> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: theme.scaffoldBackgroundColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Pending Approvals'),
|
title: const Text('Approvals'),
|
||||||
centerTitle: false,
|
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh, size: 18),
|
||||||
tooltip: 'Reload',
|
tooltip: 'Reload',
|
||||||
onPressed: _refresh,
|
onPressed: _refresh,
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: FaiSpace.sm),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: FutureBuilder<List<PendingApproval>>(
|
body: FutureBuilder<List<PendingApproval>>(
|
||||||
|
|
@ -99,59 +110,30 @@ class _ApprovalsPageState extends State<ApprovalsPage> {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
return Center(
|
return FaiEmptyState(
|
||||||
child: Padding(
|
icon: Icons.cloud_off_outlined,
|
||||||
padding: const EdgeInsets.all(32),
|
iconColor: theme.colorScheme.error,
|
||||||
child: Column(
|
title: 'Hub unreachable',
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
hint: 'Start the hub with `fai serve`.',
|
||||||
children: [
|
action: FilledButton.tonal(
|
||||||
Icon(
|
onPressed: _refresh,
|
||||||
Icons.cloud_off_outlined,
|
child: const Text('Retry'),
|
||||||
size: 48,
|
|
||||||
color: theme.colorScheme.error,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text('Hub unreachable: ${snapshot.error}'),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
FilledButton.tonal(
|
|
||||||
onPressed: _refresh,
|
|
||||||
child: const Text('Retry'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final pending = snapshot.data ?? [];
|
final pending = snapshot.data ?? [];
|
||||||
if (pending.isEmpty) {
|
if (pending.isEmpty) {
|
||||||
return Center(
|
return const FaiEmptyState(
|
||||||
child: Padding(
|
icon: Icons.task_alt_outlined,
|
||||||
padding: const EdgeInsets.all(32),
|
title: 'Inbox zero',
|
||||||
child: Column(
|
hint:
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
'All caught up. New `system.approval@^0` steps land here automatically.',
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.check_circle_outline,
|
|
||||||
size: 64,
|
|
||||||
color: theme.colorScheme.primary
|
|
||||||
.withValues(alpha: 0.5),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'No pending approvals.',
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(FaiSpace.xl),
|
||||||
itemCount: pending.length,
|
itemCount: pending.length,
|
||||||
separatorBuilder: (_, _) => const SizedBox(height: 16),
|
separatorBuilder: (_, _) => const SizedBox(height: FaiSpace.md),
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
final a = pending[i];
|
final a = pending[i];
|
||||||
return _ApprovalCard(
|
return _ApprovalCard(
|
||||||
|
|
@ -181,89 +163,90 @@ class _ApprovalCard extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return Card(
|
return FaiCard(
|
||||||
elevation: 0,
|
accentTop: true,
|
||||||
shape: RoundedRectangleBorder(
|
child: Column(
|
||||||
borderRadius: BorderRadius.circular(12),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
side: BorderSide(color: theme.colorScheme.outlineVariant),
|
children: [
|
||||||
),
|
Row(
|
||||||
child: Padding(
|
children: [
|
||||||
padding: const EdgeInsets.all(20),
|
FaiPill(
|
||||||
child: Column(
|
label: 'pending',
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
tone: FaiPillTone.warning,
|
||||||
children: [
|
icon: Icons.pending_outlined,
|
||||||
Row(
|
),
|
||||||
children: [
|
const SizedBox(width: FaiSpace.sm),
|
||||||
Icon(
|
Expanded(
|
||||||
Icons.pending_outlined,
|
child: Text(
|
||||||
color: theme.colorScheme.tertiary,
|
'${approval.flowName} › ${approval.stepId}',
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'${approval.flowName} → ${approval.stepId}',
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
),
|
||||||
if (approval.expiresAt != null)
|
if (approval.expiresAt != null)
|
||||||
Text(
|
FaiPill(
|
||||||
_expiresIn(approval.expiresAt!),
|
label: _expiresIn(approval.expiresAt!),
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
tone: FaiPillTone.neutral,
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
icon: Icons.schedule,
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
approval.prompt,
|
|
||||||
style: theme.textTheme.bodyLarge,
|
|
||||||
),
|
|
||||||
if (approval.showPreview != null) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.surfaceContainerHighest,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
),
|
||||||
child: Text(
|
],
|
||||||
approval.showPreview!,
|
),
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
const SizedBox(height: FaiSpace.lg),
|
||||||
fontFamily: 'monospace',
|
Text(
|
||||||
|
approval.prompt,
|
||||||
|
style: theme.textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
if (approval.showPreview != null) ...[
|
||||||
|
const SizedBox(height: FaiSpace.md),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(FaiSpace.md),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||||
|
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
approval.showPreview!,
|
||||||
|
style: FaiTheme.mono(
|
||||||
|
size: 11,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: FaiSpace.lg),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
approval.id,
|
||||||
|
style: FaiTheme.mono(
|
||||||
|
size: 10,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: onReject,
|
||||||
|
icon: const Icon(Icons.close, size: 16),
|
||||||
|
label: const Text('Reject'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: theme.colorScheme.error,
|
||||||
|
side: BorderSide(
|
||||||
|
color: theme.colorScheme.error.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: FaiSpace.sm),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: onApprove,
|
||||||
|
icon: const Icon(Icons.check, size: 16),
|
||||||
|
label: const Text('Approve'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
Row(
|
],
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'approval id: ${approval.id}',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: onReject,
|
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
label: const Text('Reject'),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: onApprove,
|
|
||||||
icon: const Icon(Icons.check),
|
|
||||||
label: const Text('Approve'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -272,8 +255,8 @@ class _ApprovalCard extends StatelessWidget {
|
||||||
final remaining = t.difference(DateTime.now());
|
final remaining = t.difference(DateTime.now());
|
||||||
if (remaining.isNegative) return 'expired';
|
if (remaining.isNegative) return 'expired';
|
||||||
final minutes = remaining.inMinutes;
|
final minutes = remaining.inMinutes;
|
||||||
if (minutes < 60) return 'expires in ${minutes}m';
|
if (minutes < 60) return '${minutes}m';
|
||||||
final hours = remaining.inHours;
|
final hours = remaining.inHours;
|
||||||
return 'expires in ${hours}h';
|
return '${hours}h';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../data/hub.dart';
|
import '../data/hub.dart';
|
||||||
|
import '../theme/theme.dart';
|
||||||
|
import '../theme/tokens.dart';
|
||||||
|
import '../widgets/widgets.dart';
|
||||||
|
|
||||||
class AuditPage extends StatefulWidget {
|
class AuditPage extends StatefulWidget {
|
||||||
const AuditPage({super.key});
|
const AuditPage({super.key});
|
||||||
|
|
@ -35,14 +38,8 @@ class _AuditPageState extends State<AuditPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _refresh() async {
|
Future<void> _refresh() async {
|
||||||
final types = _typeFilter == 'all'
|
|
||||||
? <String>[]
|
|
||||||
: <String>[]; // server-side filter not yet — clientside below
|
|
||||||
try {
|
try {
|
||||||
final events = await HubService.instance.recentEvents(
|
final events = await HubService.instance.recentEvents(limit: 100);
|
||||||
limit: 100,
|
|
||||||
types: types,
|
|
||||||
);
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_events = events;
|
_events = events;
|
||||||
|
|
@ -66,145 +63,70 @@ class _AuditPageState extends State<AuditPage> {
|
||||||
: _events.where((e) => e.type.startsWith(_typeFilter)).toList();
|
: _events.where((e) => e.type.startsWith(_typeFilter)).toList();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: theme.scaffoldBackgroundColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Audit Stream'),
|
title: const Text('Audit'),
|
||||||
centerTitle: false,
|
|
||||||
actions: [
|
actions: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 16),
|
padding: const EdgeInsets.only(right: FaiSpace.lg),
|
||||||
child: DropdownButton<String>(
|
child: _FilterChips(
|
||||||
value: _typeFilter,
|
value: _typeFilter,
|
||||||
onChanged: (v) => setState(() => _typeFilter = v ?? 'all'),
|
onChanged: (v) => setState(() => _typeFilter = v),
|
||||||
items: const [
|
|
||||||
DropdownMenuItem(value: 'all', child: Text('all events')),
|
|
||||||
DropdownMenuItem(value: 'flow.', child: Text('flow.*')),
|
|
||||||
DropdownMenuItem(value: 'step.', child: Text('step.*')),
|
|
||||||
DropdownMenuItem(value: 'module.', child: Text('module.*')),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
_StatusBar(eventCount: filtered.length, error: _error),
|
_LiveStatusBar(eventCount: filtered.length, error: _error),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: !_initialLoaded
|
child: !_initialLoaded
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: _error != null && _events.isEmpty
|
: _error != null && _events.isEmpty
|
||||||
? Center(
|
? FaiEmptyState(
|
||||||
child: Padding(
|
icon: Icons.cloud_off_outlined,
|
||||||
padding: const EdgeInsets.all(32),
|
iconColor: theme.colorScheme.error,
|
||||||
child: Text(
|
title: 'Hub unreachable',
|
||||||
'Hub unreachable: $_error\n\n'
|
hint: 'Start the hub with `fai serve`.',
|
||||||
'Start the hub with `fai serve`.',
|
)
|
||||||
textAlign: TextAlign.center,
|
: filtered.isEmpty
|
||||||
style: theme.textTheme.bodyMedium,
|
? const FaiEmptyState(
|
||||||
),
|
icon: Icons.timeline_outlined,
|
||||||
),
|
title: 'No events yet',
|
||||||
)
|
hint:
|
||||||
: filtered.isEmpty
|
'Run a flow to populate the audit stream.\nEvents appear here within seconds.',
|
||||||
? Center(
|
)
|
||||||
child: Text(
|
: ListView.separated(
|
||||||
'(no events yet)',
|
padding: const EdgeInsets.all(FaiSpace.xl),
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
itemCount: filtered.length,
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
separatorBuilder: (_, _) =>
|
||||||
),
|
const SizedBox(height: FaiSpace.xs),
|
||||||
),
|
itemBuilder: (context, i) {
|
||||||
)
|
final e = filtered[i];
|
||||||
: ListView.separated(
|
return FaiDataRow(
|
||||||
padding: const EdgeInsets.all(24),
|
accent: _toneFor(e.type, theme),
|
||||||
itemCount: filtered.length,
|
leading: _formatTime(e.timestamp),
|
||||||
separatorBuilder: (_, _) =>
|
title: e.type,
|
||||||
const SizedBox(height: 8),
|
subtitle: _contextLine(e),
|
||||||
itemBuilder: (context, i) {
|
trailing: e.durationMs != null
|
||||||
final e = filtered[i];
|
? '${e.durationMs}ms'
|
||||||
final ts = e.timestamp;
|
: null,
|
||||||
final tsStr =
|
);
|
||||||
'${ts.hour.toString().padLeft(2, '0')}:${ts.minute.toString().padLeft(2, '0')}:${ts.second.toString().padLeft(2, '0')}';
|
},
|
||||||
return Card(
|
|
||||||
elevation: 0,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
side: BorderSide(
|
|
||||||
color: theme.colorScheme.outlineVariant,
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 4,
|
|
||||||
height: 32,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: _toneFor(e.type, theme),
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
SizedBox(
|
|
||||||
width: 90,
|
|
||||||
child: Text(
|
|
||||||
tsStr,
|
|
||||||
style:
|
|
||||||
theme.textTheme.bodySmall?.copyWith(
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
color: theme
|
|
||||||
.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 180,
|
|
||||||
child: Text(
|
|
||||||
e.type,
|
|
||||||
style:
|
|
||||||
theme.textTheme.bodyMedium?.copyWith(
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
_contextLine(e),
|
|
||||||
style:
|
|
||||||
theme.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: theme
|
|
||||||
.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (e.durationMs != null)
|
|
||||||
Text(
|
|
||||||
'${e.durationMs}ms',
|
|
||||||
style:
|
|
||||||
theme.textTheme.bodySmall?.copyWith(
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
color: theme
|
|
||||||
.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _formatTime(DateTime ts) =>
|
||||||
|
'${ts.hour.toString().padLeft(2, '0')}:${ts.minute.toString().padLeft(2, '0')}:${ts.second.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
String _contextLine(AuditEvent e) {
|
String _contextLine(AuditEvent e) {
|
||||||
final parts = <String>[];
|
final parts = <String>[];
|
||||||
if (e.flowName != null) parts.add(e.flowName!);
|
if (e.flowName != null) parts.add(e.flowName!);
|
||||||
if (e.stepId != null) parts.add(':${e.stepId}');
|
if (e.stepId != null) parts.add(' › ${e.stepId}');
|
||||||
if (e.moduleName != null) parts.add(' via ${e.moduleName}');
|
if (e.moduleName != null) parts.add(' via ${e.moduleName}');
|
||||||
if (e.error != null) parts.add(' [error: ${e.error}]');
|
if (e.error != null) parts.add(' [error: ${e.error}]');
|
||||||
return parts.join('');
|
return parts.join('');
|
||||||
|
|
@ -213,16 +135,112 @@ class _AuditPageState extends State<AuditPage> {
|
||||||
Color _toneFor(String type, ThemeData theme) {
|
Color _toneFor(String type, ThemeData theme) {
|
||||||
if (type.endsWith('.failed')) return theme.colorScheme.error;
|
if (type.endsWith('.failed')) return theme.colorScheme.error;
|
||||||
if (type.endsWith('.completed')) return theme.colorScheme.primary;
|
if (type.endsWith('.completed')) return theme.colorScheme.primary;
|
||||||
if (type.endsWith('.started')) return theme.colorScheme.tertiary;
|
if (type.endsWith('.started')) return FaiColors.warning;
|
||||||
return theme.colorScheme.outline;
|
return theme.colorScheme.outline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StatusBar extends StatelessWidget {
|
class _FilterChips extends StatelessWidget {
|
||||||
|
final String value;
|
||||||
|
final ValueChanged<String> onChanged;
|
||||||
|
|
||||||
|
const _FilterChips({required this.value, required this.onChanged});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
const items = [
|
||||||
|
('all', 'all'),
|
||||||
|
('flow.', 'flow'),
|
||||||
|
('step.', 'step'),
|
||||||
|
('module.', 'module'),
|
||||||
|
];
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
for (final (v, label) in items)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: FaiSpace.xs),
|
||||||
|
child: _ChipButton(
|
||||||
|
label: label,
|
||||||
|
selected: value == v,
|
||||||
|
onTap: () => onChanged(v),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChipButton extends StatefulWidget {
|
||||||
|
final String label;
|
||||||
|
final bool selected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _ChipButton({
|
||||||
|
required this.label,
|
||||||
|
required this.selected,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ChipButton> createState() => _ChipButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChipButtonState extends State<_ChipButton> {
|
||||||
|
bool _hovered = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final fg = widget.selected
|
||||||
|
? theme.colorScheme.primary
|
||||||
|
: _hovered
|
||||||
|
? theme.colorScheme.onSurface
|
||||||
|
: theme.colorScheme.onSurfaceVariant;
|
||||||
|
final bg = widget.selected
|
||||||
|
? theme.colorScheme.primary.withValues(alpha: 0.12)
|
||||||
|
: _hovered
|
||||||
|
? theme.colorScheme.surfaceContainerHigh
|
||||||
|
: Colors.transparent;
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: (_) => setState(() => _hovered = true),
|
||||||
|
onExit: (_) => setState(() => _hovered = false),
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: widget.onTap,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: FaiMotion.fast,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: FaiSpace.md,
|
||||||
|
vertical: 6,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bg,
|
||||||
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||||
|
border: Border.all(
|
||||||
|
color: widget.selected
|
||||||
|
? theme.colorScheme.primary.withValues(alpha: 0.3)
|
||||||
|
: Colors.transparent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
widget.label,
|
||||||
|
style: FaiTheme.mono(
|
||||||
|
size: 11,
|
||||||
|
weight: widget.selected ? FontWeight.w500 : FontWeight.w400,
|
||||||
|
color: fg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LiveStatusBar extends StatelessWidget {
|
||||||
final int eventCount;
|
final int eventCount;
|
||||||
final String? error;
|
final String? error;
|
||||||
|
|
||||||
const _StatusBar({required this.eventCount, required this.error});
|
const _LiveStatusBar({required this.eventCount, required this.error});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -230,22 +248,50 @@ class _StatusBar extends StatelessWidget {
|
||||||
final live = error == null;
|
final live = error == null;
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
padding: const EdgeInsets.symmetric(
|
||||||
color: theme.colorScheme.surfaceContainerHigh,
|
horizontal: FaiSpace.xl,
|
||||||
|
vertical: FaiSpace.sm,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerHigh,
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(color: theme.colorScheme.outlineVariant),
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
FaiStatusDot(
|
||||||
Icons.fiber_manual_record,
|
color: live ? FaiColors.success : FaiColors.danger,
|
||||||
size: 12,
|
pulsing: live,
|
||||||
color: live ? theme.colorScheme.tertiary : theme.colorScheme.error,
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
Text(
|
Text(
|
||||||
live
|
live
|
||||||
? 'live (polling 2s) — $eventCount events'
|
? 'live · polling 2s · $eventCount event${eventCount == 1 ? '' : 's'}'
|
||||||
: 'disconnected — $error',
|
: 'disconnected · $error',
|
||||||
style: theme.textTheme.bodySmall,
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (live)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.shield_outlined,
|
||||||
|
size: 12,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'hash chain verified',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../data/hub.dart';
|
import '../data/hub.dart';
|
||||||
|
import '../theme/tokens.dart';
|
||||||
|
import '../widgets/widgets.dart';
|
||||||
|
|
||||||
class ModulesPage extends StatefulWidget {
|
class ModulesPage extends StatefulWidget {
|
||||||
const ModulesPage({super.key});
|
const ModulesPage({super.key});
|
||||||
|
|
@ -24,17 +26,17 @@ class _ModulesPageState extends State<ModulesPage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Installed Modules'),
|
title: const Text('Modules'),
|
||||||
centerTitle: false,
|
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh, size: 18),
|
||||||
tooltip: 'Reload',
|
tooltip: 'Reload',
|
||||||
onPressed: _refresh,
|
onPressed: _refresh,
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: FaiSpace.sm),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: FutureBuilder<List<ModuleSummary>>(
|
body: FutureBuilder<List<ModuleSummary>>(
|
||||||
|
|
@ -44,78 +46,32 @@ class _ModulesPageState extends State<ModulesPage> {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
return _ConnectionError(
|
return FaiEmptyState(
|
||||||
message: 'Hub unreachable: ${snapshot.error}',
|
icon: Icons.cloud_off_outlined,
|
||||||
onRetry: _refresh,
|
iconColor: Theme.of(context).colorScheme.error,
|
||||||
|
title: 'Hub unreachable',
|
||||||
|
hint:
|
||||||
|
'Start the hub with `fai serve`. Studio will retry automatically.',
|
||||||
|
action: FilledButton.tonal(
|
||||||
|
onPressed: _refresh,
|
||||||
|
child: const Text('Retry'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final modules = snapshot.data ?? [];
|
final modules = snapshot.data ?? [];
|
||||||
if (modules.isEmpty) {
|
if (modules.isEmpty) {
|
||||||
return Center(
|
return const FaiEmptyState(
|
||||||
child: Padding(
|
icon: Icons.extension_outlined,
|
||||||
padding: const EdgeInsets.all(32),
|
title: 'No modules yet',
|
||||||
child: Text(
|
hint:
|
||||||
'No modules installed.\n'
|
|
||||||
'Run `fai install <capability-name>` or check ~/.fai/modules/.',
|
'Run `fai install <capability-name>` or check ~/.fai/modules/.',
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(FaiSpace.xl),
|
||||||
itemCount: modules.length,
|
itemCount: modules.length,
|
||||||
separatorBuilder: (_, _) => const SizedBox(height: 12),
|
separatorBuilder: (_, _) => const SizedBox(height: FaiSpace.md),
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) => _ModuleCard(module: modules[i]),
|
||||||
final m = modules[i];
|
|
||||||
return Card(
|
|
||||||
elevation: 0,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
side: BorderSide(color: theme.colorScheme.outlineVariant),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
m.name,
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
vertical: 2,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.secondaryContainer,
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'v${m.version}',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSecondaryContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_ChipRow(label: 'Capabilities', items: m.capabilities),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -123,99 +79,68 @@ class _ModulesPageState extends State<ModulesPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChipRow extends StatelessWidget {
|
class _ModuleCard extends StatelessWidget {
|
||||||
final String label;
|
final ModuleSummary module;
|
||||||
final List<String> items;
|
|
||||||
|
|
||||||
const _ChipRow({required this.label, required this.items});
|
const _ModuleCard({required this.module});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return Row(
|
return FaiCard(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
SizedBox(
|
children: [
|
||||||
width: 140,
|
Row(
|
||||||
child: Text(
|
children: [
|
||||||
label,
|
Text(
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
module.name,
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
),
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Wrap(
|
|
||||||
spacing: 6,
|
|
||||||
runSpacing: 6,
|
|
||||||
children: items
|
|
||||||
.map(
|
|
||||||
(s) => Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
vertical: 3,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.surfaceContainerHighest,
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
s,
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConnectionError extends StatelessWidget {
|
|
||||||
final String message;
|
|
||||||
final VoidCallback onRetry;
|
|
||||||
|
|
||||||
const _ConnectionError({required this.message, required this.onRetry});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
return Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(32),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.cloud_off_outlined,
|
|
||||||
size: 48,
|
|
||||||
color: theme.colorScheme.error,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
message,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Start the hub with `fai serve`.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
const SizedBox(height: 16),
|
FaiPill(
|
||||||
FilledButton.tonal(
|
label: 'v${module.version}',
|
||||||
onPressed: onRetry,
|
tone: FaiPillTone.neutral,
|
||||||
child: const Text('Retry'),
|
monospace: true,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: FaiSpace.md),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 110,
|
||||||
|
child: Text(
|
||||||
|
'Capabilities',
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
letterSpacing: 0.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Wrap(
|
||||||
|
spacing: FaiSpace.xs,
|
||||||
|
runSpacing: FaiSpace.xs,
|
||||||
|
children: module.capabilities
|
||||||
|
.map(
|
||||||
|
(c) => FaiPill(
|
||||||
|
label: c,
|
||||||
|
tone: FaiPillTone.accent,
|
||||||
|
monospace: true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
236
lib/theme/theme.dart
Normal file
236
lib/theme/theme.dart
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
// Theme assembly. ColorScheme + typography + component themes.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
import 'tokens.dart';
|
||||||
|
|
||||||
|
class FaiTheme {
|
||||||
|
FaiTheme._();
|
||||||
|
|
||||||
|
/// Inter for UI, JetBrains Mono for technical strings (IDs,
|
||||||
|
/// paths, capability refs). Both via google_fonts so they
|
||||||
|
/// bundle on first run; for production we'd ship them as
|
||||||
|
/// app assets.
|
||||||
|
static TextTheme _textTheme(Brightness b) {
|
||||||
|
final base = b == Brightness.dark
|
||||||
|
? Typography.whiteCupertino
|
||||||
|
: Typography.blackCupertino;
|
||||||
|
return GoogleFonts.interTextTheme(base).copyWith(
|
||||||
|
displaySmall: GoogleFonts.inter(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: -0.4,
|
||||||
|
),
|
||||||
|
headlineSmall: GoogleFonts.inter(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: -0.2,
|
||||||
|
),
|
||||||
|
titleMedium: GoogleFonts.inter(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: 0,
|
||||||
|
),
|
||||||
|
titleSmall: GoogleFonts.inter(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
bodyLarge: GoogleFonts.inter(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
height: 1.45,
|
||||||
|
),
|
||||||
|
bodyMedium: GoogleFonts.inter(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
bodySmall: GoogleFonts.inter(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
labelMedium: GoogleFonts.inter(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
labelSmall: GoogleFonts.inter(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
letterSpacing: 0.2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// JetBrains Mono for monospaced technical content. Used by
|
||||||
|
/// FaiMono helper.
|
||||||
|
static TextStyle mono({
|
||||||
|
double size = 12,
|
||||||
|
FontWeight weight = FontWeight.w400,
|
||||||
|
Color? color,
|
||||||
|
}) =>
|
||||||
|
GoogleFonts.jetBrainsMono(
|
||||||
|
fontSize: size,
|
||||||
|
fontWeight: weight,
|
||||||
|
color: color,
|
||||||
|
height: 1.4,
|
||||||
|
);
|
||||||
|
|
||||||
|
static ThemeData dark() {
|
||||||
|
final scheme = ColorScheme(
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
primary: FaiColors.accent,
|
||||||
|
onPrimary: FaiColors.black,
|
||||||
|
primaryContainer: FaiColors.accentMuted,
|
||||||
|
onPrimaryContainer: FaiColors.textStrong,
|
||||||
|
secondary: FaiColors.accentDeep,
|
||||||
|
onSecondary: FaiColors.black,
|
||||||
|
secondaryContainer: FaiColors.surfaceHigh,
|
||||||
|
onSecondaryContainer: FaiColors.text,
|
||||||
|
tertiary: FaiColors.accent,
|
||||||
|
onTertiary: FaiColors.black,
|
||||||
|
tertiaryContainer: FaiColors.surfaceHigh,
|
||||||
|
onTertiaryContainer: FaiColors.text,
|
||||||
|
error: FaiColors.danger,
|
||||||
|
onError: FaiColors.textStrong,
|
||||||
|
errorContainer: const Color(0xFF3F1518),
|
||||||
|
onErrorContainer: const Color(0xFFFCA5A5),
|
||||||
|
surface: FaiColors.black,
|
||||||
|
onSurface: FaiColors.text,
|
||||||
|
onSurfaceVariant: FaiColors.muted,
|
||||||
|
outline: FaiColors.border,
|
||||||
|
outlineVariant: FaiColors.border,
|
||||||
|
surfaceContainerLowest: FaiColors.black,
|
||||||
|
surfaceContainerLow: FaiColors.surface,
|
||||||
|
surfaceContainer: FaiColors.surface,
|
||||||
|
surfaceContainerHigh: FaiColors.surfaceHigh,
|
||||||
|
surfaceContainerHighest: FaiColors.surfaceHigh,
|
||||||
|
);
|
||||||
|
return _build(scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ThemeData light() {
|
||||||
|
final scheme = ColorScheme(
|
||||||
|
brightness: Brightness.light,
|
||||||
|
primary: FaiColors.accentDeep,
|
||||||
|
onPrimary: FaiColors.lightSurface,
|
||||||
|
primaryContainer: const Color(0xFFE0F2FE),
|
||||||
|
onPrimaryContainer: FaiColors.accentMuted,
|
||||||
|
secondary: FaiColors.accentMuted,
|
||||||
|
onSecondary: FaiColors.lightSurface,
|
||||||
|
secondaryContainer: FaiColors.lightSurfaceHigh,
|
||||||
|
onSecondaryContainer: FaiColors.lightText,
|
||||||
|
tertiary: FaiColors.accentDeep,
|
||||||
|
onTertiary: FaiColors.lightSurface,
|
||||||
|
tertiaryContainer: const Color(0xFFE0F2FE),
|
||||||
|
onTertiaryContainer: FaiColors.accentMuted,
|
||||||
|
error: FaiColors.danger,
|
||||||
|
onError: FaiColors.lightSurface,
|
||||||
|
errorContainer: const Color(0xFFFEE2E2),
|
||||||
|
onErrorContainer: const Color(0xFF7F1D1D),
|
||||||
|
surface: FaiColors.lightCanvas,
|
||||||
|
onSurface: FaiColors.lightText,
|
||||||
|
onSurfaceVariant: FaiColors.lightMuted,
|
||||||
|
outline: FaiColors.lightBorder,
|
||||||
|
outlineVariant: FaiColors.lightBorder,
|
||||||
|
surfaceContainerLowest: FaiColors.lightCanvas,
|
||||||
|
surfaceContainerLow: FaiColors.lightSurface,
|
||||||
|
surfaceContainer: FaiColors.lightSurface,
|
||||||
|
surfaceContainerHigh: FaiColors.lightSurfaceHigh,
|
||||||
|
surfaceContainerHighest: FaiColors.lightSurfaceHigh,
|
||||||
|
);
|
||||||
|
return _build(scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ThemeData _build(ColorScheme scheme) {
|
||||||
|
final textTheme = _textTheme(scheme.brightness);
|
||||||
|
return ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
colorScheme: scheme,
|
||||||
|
scaffoldBackgroundColor: scheme.surface,
|
||||||
|
textTheme: textTheme,
|
||||||
|
primaryTextTheme: textTheme,
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
backgroundColor: scheme.surface,
|
||||||
|
foregroundColor: scheme.onSurface,
|
||||||
|
elevation: 0,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
centerTitle: false,
|
||||||
|
titleTextStyle: textTheme.headlineSmall,
|
||||||
|
toolbarHeight: 64,
|
||||||
|
),
|
||||||
|
cardTheme: CardThemeData(
|
||||||
|
color: scheme.surfaceContainer,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(FaiRadius.md),
|
||||||
|
side: BorderSide(color: scheme.outlineVariant),
|
||||||
|
),
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
filledButtonTheme: FilledButtonThemeData(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: scheme.primary,
|
||||||
|
foregroundColor: scheme.onPrimary,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: FaiSpace.lg,
|
||||||
|
vertical: FaiSpace.md,
|
||||||
|
),
|
||||||
|
textStyle: textTheme.labelMedium,
|
||||||
|
animationDuration: FaiMotion.fast,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: scheme.onSurface,
|
||||||
|
side: BorderSide(color: scheme.outline),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: FaiSpace.lg,
|
||||||
|
vertical: FaiSpace.md,
|
||||||
|
),
|
||||||
|
textStyle: textTheme.labelMedium,
|
||||||
|
animationDuration: FaiMotion.fast,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
iconButtonTheme: IconButtonThemeData(
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
foregroundColor: scheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
navigationRailTheme: NavigationRailThemeData(
|
||||||
|
backgroundColor: scheme.surfaceContainerLow,
|
||||||
|
indicatorColor: scheme.primaryContainer,
|
||||||
|
selectedIconTheme: IconThemeData(color: scheme.primary, size: 22),
|
||||||
|
unselectedIconTheme:
|
||||||
|
IconThemeData(color: scheme.onSurfaceVariant, size: 22),
|
||||||
|
selectedLabelTextStyle:
|
||||||
|
textTheme.labelMedium?.copyWith(color: scheme.primary),
|
||||||
|
unselectedLabelTextStyle: textTheme.labelMedium
|
||||||
|
?.copyWith(color: scheme.onSurfaceVariant),
|
||||||
|
useIndicator: true,
|
||||||
|
),
|
||||||
|
dividerColor: scheme.outlineVariant,
|
||||||
|
dividerTheme: DividerThemeData(
|
||||||
|
color: scheme.outlineVariant,
|
||||||
|
thickness: 1,
|
||||||
|
space: 1,
|
||||||
|
),
|
||||||
|
snackBarTheme: SnackBarThemeData(
|
||||||
|
backgroundColor: scheme.surfaceContainerHigh,
|
||||||
|
contentTextStyle:
|
||||||
|
textTheme.bodyMedium?.copyWith(color: scheme.onSurface),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
lib/theme/tokens.dart
Normal file
71
lib/theme/tokens.dart
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
// Design tokens for F∆I Studio.
|
||||||
|
//
|
||||||
|
// One file, semantic names, no magic numbers in the rest of the
|
||||||
|
// codebase. Both light and dark modes derive from these tokens.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Colour palette. Single strong accent (F∆I Cyan) plus neutral
|
||||||
|
/// greys. Status colours stay restrained — no candy palette.
|
||||||
|
class FaiColors {
|
||||||
|
FaiColors._();
|
||||||
|
|
||||||
|
// Brand accent. Slightly cooler / more saturated than the
|
||||||
|
// initial #1E3A8A — this reads more as "deep tech, alive"
|
||||||
|
// than the previous corporate navy.
|
||||||
|
static const accent = Color(0xFF38BDF8); // sky-400 / cyan
|
||||||
|
static const accentDeep = Color(0xFF0EA5E9); // sky-500
|
||||||
|
static const accentMuted = Color(0xFF0369A1); // sky-700
|
||||||
|
|
||||||
|
// Semantic status. Used sparingly: tones, not flat fills.
|
||||||
|
static const success = Color(0xFF22C55E); // green-500
|
||||||
|
static const warning = Color(0xFFF59E0B); // amber-500
|
||||||
|
static const danger = Color(0xFFEF4444); // red-500
|
||||||
|
|
||||||
|
// Neutral scale. Dark-mode first.
|
||||||
|
static const black = Color(0xFF09090B); // canvas
|
||||||
|
static const surface = Color(0xFF18181B); // cards
|
||||||
|
static const surfaceHigh = Color(0xFF27272A); // elevated
|
||||||
|
static const border = Color(0xFF3F3F46); // 1px outlines
|
||||||
|
static const muted = Color(0xFF71717A); // de-emphasised text
|
||||||
|
static const text = Color(0xFFE4E4E7); // body
|
||||||
|
static const textStrong = Color(0xFFFAFAFA); // headings
|
||||||
|
|
||||||
|
// Light-mode neutrals. Calmer than pure white.
|
||||||
|
static const lightCanvas = Color(0xFFFAFAFA);
|
||||||
|
static const lightSurface = Color(0xFFFFFFFF);
|
||||||
|
static const lightSurfaceHigh = Color(0xFFF4F4F5);
|
||||||
|
static const lightBorder = Color(0xFFE4E4E7);
|
||||||
|
static const lightMuted = Color(0xFF71717A);
|
||||||
|
static const lightText = Color(0xFF18181B);
|
||||||
|
static const lightTextStrong = Color(0xFF09090B);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spacing scale. Stick to multiples of 4. Keep the vocabulary
|
||||||
|
/// small — six steps cover everything.
|
||||||
|
class FaiSpace {
|
||||||
|
FaiSpace._();
|
||||||
|
static const xs = 4.0;
|
||||||
|
static const sm = 8.0;
|
||||||
|
static const md = 12.0;
|
||||||
|
static const lg = 16.0;
|
||||||
|
static const xl = 24.0;
|
||||||
|
static const xxl = 32.0;
|
||||||
|
static const xxxl = 48.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Border radii. Two sizes only.
|
||||||
|
class FaiRadius {
|
||||||
|
FaiRadius._();
|
||||||
|
static const sm = 6.0;
|
||||||
|
static const md = 10.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Animation durations. Keep them under 300ms for power-user feel.
|
||||||
|
class FaiMotion {
|
||||||
|
FaiMotion._();
|
||||||
|
static const fast = Duration(milliseconds: 120);
|
||||||
|
static const base = Duration(milliseconds: 200);
|
||||||
|
static const slow = Duration(milliseconds: 320);
|
||||||
|
static const easing = Curves.easeOutCubic;
|
||||||
|
}
|
||||||
83
lib/widgets/fai_card.dart
Normal file
83
lib/widgets/fai_card.dart
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
// FaiCard — flat card with subtle accent gradient on the top
|
||||||
|
// border. Replaces Material's default Card for the F∆I look.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../theme/tokens.dart';
|
||||||
|
|
||||||
|
class FaiCard extends StatelessWidget {
|
||||||
|
/// Card body. Padding is applied internally; pass plain content.
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// When true, paint a 2px accent-coloured stripe along the top
|
||||||
|
/// edge. Used by foreground / "this matters" cards (a pending
|
||||||
|
/// approval, a failed step).
|
||||||
|
final bool accentTop;
|
||||||
|
|
||||||
|
/// When true, paint a faint vertical accent stripe on the left.
|
||||||
|
/// Used by status rows (live event, healthy service).
|
||||||
|
final Color? accentLeft;
|
||||||
|
|
||||||
|
/// Internal padding. Defaults to [FaiSpace.lg].
|
||||||
|
final EdgeInsetsGeometry padding;
|
||||||
|
|
||||||
|
const FaiCard({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.accentTop = false,
|
||||||
|
this.accentLeft,
|
||||||
|
this.padding = const EdgeInsets.all(FaiSpace.lg),
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(FaiRadius.md),
|
||||||
|
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
if (accentTop)
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: 2,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
theme.colorScheme.primary.withValues(alpha: 0.0),
|
||||||
|
theme.colorScheme.primary,
|
||||||
|
theme.colorScheme.primary.withValues(alpha: 0.0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (accentLeft != null)
|
||||||
|
Positioned(
|
||||||
|
top: FaiSpace.md,
|
||||||
|
bottom: FaiSpace.md,
|
||||||
|
left: 0,
|
||||||
|
width: 3,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: accentLeft,
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topRight: Radius.circular(2),
|
||||||
|
bottomRight: Radius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(padding: padding, child: child),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
134
lib/widgets/fai_data_row.dart
Normal file
134
lib/widgets/fai_data_row.dart
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
// FaiDataRow — Linear-style dense list row used by the audit
|
||||||
|
// stream. Hover-elevation, accent-coloured leading stripe, mono
|
||||||
|
// timestamp, expressive type badge.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../theme/theme.dart';
|
||||||
|
import '../theme/tokens.dart';
|
||||||
|
|
||||||
|
class FaiDataRow extends StatefulWidget {
|
||||||
|
/// Coloured leading stripe (4px). Used to encode event type
|
||||||
|
/// (success / warning / failure) at a glance.
|
||||||
|
final Color accent;
|
||||||
|
|
||||||
|
/// Left-most column: a short, monospaced label (timestamp, id).
|
||||||
|
final String leading;
|
||||||
|
|
||||||
|
/// Title in the middle. Usually the event type, in mono.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// Subtitle on the right of title. Free-form, lighter colour.
|
||||||
|
final String subtitle;
|
||||||
|
|
||||||
|
/// Right-most column: trailing label (duration, etc).
|
||||||
|
final String? trailing;
|
||||||
|
|
||||||
|
/// Optional tap handler.
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
const FaiDataRow({
|
||||||
|
super.key,
|
||||||
|
required this.accent,
|
||||||
|
required this.leading,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
this.trailing,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FaiDataRow> createState() => _FaiDataRowState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FaiDataRowState extends State<FaiDataRow> {
|
||||||
|
bool _hovered = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: (_) => setState(() => _hovered = true),
|
||||||
|
onExit: (_) => setState(() => _hovered = false),
|
||||||
|
cursor: widget.onTap != null
|
||||||
|
? SystemMouseCursors.click
|
||||||
|
: SystemMouseCursors.basic,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: widget.onTap,
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: FaiMotion.fast,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _hovered
|
||||||
|
? theme.colorScheme.surfaceContainerHigh
|
||||||
|
: theme.colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||||
|
border: Border.all(
|
||||||
|
color: _hovered
|
||||||
|
? theme.colorScheme.outline
|
||||||
|
: theme.colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: FaiSpace.md,
|
||||||
|
vertical: FaiSpace.sm,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 3,
|
||||||
|
height: 24,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: widget.accent,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: FaiSpace.md),
|
||||||
|
SizedBox(
|
||||||
|
width: 88,
|
||||||
|
child: Text(
|
||||||
|
widget.leading,
|
||||||
|
style: FaiTheme.mono(
|
||||||
|
size: 11,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: Text(
|
||||||
|
widget.title,
|
||||||
|
style: FaiTheme.mono(
|
||||||
|
size: 12,
|
||||||
|
weight: FontWeight.w500,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.subtitle,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.trailing != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: FaiSpace.md),
|
||||||
|
child: Text(
|
||||||
|
widget.trailing!,
|
||||||
|
style: FaiTheme.mono(
|
||||||
|
size: 11,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
162
lib/widgets/fai_delta_mark.dart
Normal file
162
lib/widgets/fai_delta_mark.dart
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
// FaiDeltaMark — the F∆I signature element. A precise triangle
|
||||||
|
// (∆) drawn from primitives, not a font glyph. Three states:
|
||||||
|
//
|
||||||
|
// - idle: static, accent colour, soft glow
|
||||||
|
// - live: gentle pulse, like a heartbeat
|
||||||
|
// - busy: rotates slowly, used while a long operation runs
|
||||||
|
//
|
||||||
|
// Sized small. Lives in the navigation rail header so it's
|
||||||
|
// always visible and identifies the brand without screaming.
|
||||||
|
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
enum FaiDeltaMode { idle, live, busy }
|
||||||
|
|
||||||
|
class FaiDeltaMark extends StatefulWidget {
|
||||||
|
final FaiDeltaMode mode;
|
||||||
|
final double size;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
const FaiDeltaMark({
|
||||||
|
super.key,
|
||||||
|
required this.color,
|
||||||
|
this.mode = FaiDeltaMode.idle,
|
||||||
|
this.size = 36,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FaiDeltaMark> createState() => _FaiDeltaMarkState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FaiDeltaMarkState extends State<FaiDeltaMark>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _ctrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_ctrl = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: _durationFor(widget.mode),
|
||||||
|
);
|
||||||
|
_restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration _durationFor(FaiDeltaMode m) {
|
||||||
|
switch (m) {
|
||||||
|
case FaiDeltaMode.idle:
|
||||||
|
return const Duration(seconds: 1);
|
||||||
|
case FaiDeltaMode.live:
|
||||||
|
return const Duration(milliseconds: 1600);
|
||||||
|
case FaiDeltaMode.busy:
|
||||||
|
return const Duration(seconds: 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _restart() {
|
||||||
|
_ctrl.duration = _durationFor(widget.mode);
|
||||||
|
switch (widget.mode) {
|
||||||
|
case FaiDeltaMode.idle:
|
||||||
|
_ctrl.stop();
|
||||||
|
_ctrl.value = 0;
|
||||||
|
break;
|
||||||
|
case FaiDeltaMode.live:
|
||||||
|
_ctrl.repeat(reverse: true);
|
||||||
|
break;
|
||||||
|
case FaiDeltaMode.busy:
|
||||||
|
_ctrl.repeat();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant FaiDeltaMark old) {
|
||||||
|
super.didUpdateWidget(old);
|
||||||
|
if (old.mode != widget.mode) _restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_ctrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _ctrl,
|
||||||
|
builder: (_, _) {
|
||||||
|
return CustomPaint(
|
||||||
|
size: Size.square(widget.size),
|
||||||
|
painter: _DeltaPainter(
|
||||||
|
color: widget.color,
|
||||||
|
mode: widget.mode,
|
||||||
|
t: _ctrl.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DeltaPainter extends CustomPainter {
|
||||||
|
final Color color;
|
||||||
|
final FaiDeltaMode mode;
|
||||||
|
final double t;
|
||||||
|
|
||||||
|
_DeltaPainter({required this.color, required this.mode, required this.t});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final cx = size.width / 2;
|
||||||
|
final cy = size.height / 2;
|
||||||
|
final r = size.width * 0.42;
|
||||||
|
|
||||||
|
canvas.save();
|
||||||
|
canvas.translate(cx, cy);
|
||||||
|
if (mode == FaiDeltaMode.busy) {
|
||||||
|
canvas.rotate(t * 2 * pi);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outer triangle path.
|
||||||
|
final path = Path()
|
||||||
|
..moveTo(0, -r)
|
||||||
|
..lineTo(r * cos(pi / 6), r * sin(pi / 6))
|
||||||
|
..lineTo(-r * cos(pi / 6), r * sin(pi / 6))
|
||||||
|
..close();
|
||||||
|
|
||||||
|
// Glow halo for live / busy mode.
|
||||||
|
if (mode != FaiDeltaMode.idle) {
|
||||||
|
final pulseStrength = mode == FaiDeltaMode.live ? t : 1.0;
|
||||||
|
final glow = Paint()
|
||||||
|
..color = color.withValues(alpha: 0.25 + 0.25 * pulseStrength)
|
||||||
|
..maskFilter =
|
||||||
|
MaskFilter.blur(BlurStyle.normal, 6 + 6 * pulseStrength)
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
canvas.drawPath(path, glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outline stroke.
|
||||||
|
final stroke = Paint()
|
||||||
|
..color = color
|
||||||
|
..strokeWidth = 2
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeJoin = StrokeJoin.round;
|
||||||
|
canvas.drawPath(path, stroke);
|
||||||
|
|
||||||
|
// Inner accent dot — pulses opacity in live mode.
|
||||||
|
final dotAlpha = mode == FaiDeltaMode.live ? 0.4 + 0.6 * t : 1.0;
|
||||||
|
final dot = Paint()
|
||||||
|
..color = color.withValues(alpha: dotAlpha)
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
canvas.drawCircle(Offset(0, r * 0.15), r * 0.14, dot);
|
||||||
|
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _DeltaPainter old) =>
|
||||||
|
old.t != t || old.mode != mode || old.color != color;
|
||||||
|
}
|
||||||
75
lib/widgets/fai_empty_state.dart
Normal file
75
lib/widgets/fai_empty_state.dart
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
// FaiEmptyState — gracious empty / loading / error placeholder.
|
||||||
|
// Replaces "(no data)" strings with one tone, one icon, one tip.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../theme/tokens.dart';
|
||||||
|
|
||||||
|
class FaiEmptyState extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final String? hint;
|
||||||
|
final Widget? action;
|
||||||
|
|
||||||
|
/// Icon tone. Defaults to muted; set to [Theme.colorScheme.error]
|
||||||
|
/// for connection-failure variants.
|
||||||
|
final Color? iconColor;
|
||||||
|
|
||||||
|
const FaiEmptyState({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
this.hint,
|
||||||
|
this.action,
|
||||||
|
this.iconColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final color = iconColor ?? theme.colorScheme.onSurfaceVariant;
|
||||||
|
return Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 420),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(FaiSpace.xxl),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(icon, size: 24, color: color),
|
||||||
|
),
|
||||||
|
const SizedBox(height: FaiSpace.lg),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
if (hint != null) ...[
|
||||||
|
const SizedBox(height: FaiSpace.sm),
|
||||||
|
Text(
|
||||||
|
hint!,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (action != null) ...[
|
||||||
|
const SizedBox(height: FaiSpace.lg),
|
||||||
|
action!,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
lib/widgets/fai_pill.dart
Normal file
90
lib/widgets/fai_pill.dart
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
// FaiPill — small inline label. Used for capabilities, versions,
|
||||||
|
// permission scopes, status text. Replaces ad-hoc Container chips.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../theme/theme.dart';
|
||||||
|
import '../theme/tokens.dart';
|
||||||
|
|
||||||
|
enum FaiPillTone {
|
||||||
|
neutral,
|
||||||
|
accent,
|
||||||
|
success,
|
||||||
|
warning,
|
||||||
|
danger,
|
||||||
|
}
|
||||||
|
|
||||||
|
class FaiPill extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final FaiPillTone tone;
|
||||||
|
final IconData? icon;
|
||||||
|
final bool monospace;
|
||||||
|
|
||||||
|
const FaiPill({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
this.tone = FaiPillTone.neutral,
|
||||||
|
this.icon,
|
||||||
|
this.monospace = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final (bg, fg) = _colors(theme.colorScheme);
|
||||||
|
final textStyle = monospace
|
||||||
|
? FaiTheme.mono(size: 11, weight: FontWeight.w500, color: fg)
|
||||||
|
: theme.textTheme.labelSmall?.copyWith(color: fg);
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: FaiSpace.sm,
|
||||||
|
vertical: 3,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bg,
|
||||||
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (icon != null) ...[
|
||||||
|
Icon(icon, size: 11, color: fg),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
],
|
||||||
|
Text(label, style: textStyle),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
(Color, Color) _colors(ColorScheme scheme) {
|
||||||
|
switch (tone) {
|
||||||
|
case FaiPillTone.neutral:
|
||||||
|
return (scheme.surfaceContainerHighest, scheme.onSurfaceVariant);
|
||||||
|
case FaiPillTone.accent:
|
||||||
|
return (
|
||||||
|
scheme.primary.withValues(alpha: 0.15),
|
||||||
|
scheme.primary,
|
||||||
|
);
|
||||||
|
case FaiPillTone.success:
|
||||||
|
return (
|
||||||
|
FaiColors.success.withValues(alpha: 0.15),
|
||||||
|
scheme.brightness == Brightness.dark
|
||||||
|
? FaiColors.success
|
||||||
|
: const Color(0xFF15803D),
|
||||||
|
);
|
||||||
|
case FaiPillTone.warning:
|
||||||
|
return (
|
||||||
|
FaiColors.warning.withValues(alpha: 0.15),
|
||||||
|
scheme.brightness == Brightness.dark
|
||||||
|
? FaiColors.warning
|
||||||
|
: const Color(0xFFB45309),
|
||||||
|
);
|
||||||
|
case FaiPillTone.danger:
|
||||||
|
return (
|
||||||
|
scheme.errorContainer,
|
||||||
|
scheme.onErrorContainer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
101
lib/widgets/fai_status_dot.dart
Normal file
101
lib/widgets/fai_status_dot.dart
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
// FaiStatusDot — small pulsing dot used as a "live" indicator.
|
||||||
|
// Goes still when [pulsing] is false, breathes gently when true.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../theme/tokens.dart';
|
||||||
|
|
||||||
|
class FaiStatusDot extends StatefulWidget {
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
/// When true, the dot fades between full and 30% opacity.
|
||||||
|
/// When false, it stays at full opacity (steady-state).
|
||||||
|
final bool pulsing;
|
||||||
|
|
||||||
|
final double size;
|
||||||
|
|
||||||
|
const FaiStatusDot({
|
||||||
|
super.key,
|
||||||
|
required this.color,
|
||||||
|
this.pulsing = false,
|
||||||
|
this.size = 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FaiStatusDot> createState() => _FaiStatusDotState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FaiStatusDotState extends State<FaiStatusDot>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _ctrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_ctrl = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 1400),
|
||||||
|
);
|
||||||
|
if (widget.pulsing) _ctrl.repeat(reverse: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant FaiStatusDot old) {
|
||||||
|
super.didUpdateWidget(old);
|
||||||
|
if (widget.pulsing && !_ctrl.isAnimating) {
|
||||||
|
_ctrl.repeat(reverse: true);
|
||||||
|
} else if (!widget.pulsing && _ctrl.isAnimating) {
|
||||||
|
_ctrl.stop();
|
||||||
|
_ctrl.value = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_ctrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _ctrl,
|
||||||
|
builder: (_, _) {
|
||||||
|
final t = widget.pulsing ? _ctrl.value : 0.0;
|
||||||
|
final alpha = 1.0 - (t * 0.6);
|
||||||
|
return Container(
|
||||||
|
width: widget.size,
|
||||||
|
height: widget.size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: widget.color.withValues(alpha: alpha),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: widget.pulsing
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: widget.color.withValues(alpha: 0.4 * alpha),
|
||||||
|
blurRadius: 6 * (1 + t),
|
||||||
|
spreadRadius: 1 * t,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: const [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience presets.
|
||||||
|
class FaiStatusDots {
|
||||||
|
FaiStatusDots._();
|
||||||
|
static Widget live() => const FaiStatusDot(
|
||||||
|
color: FaiColors.success,
|
||||||
|
pulsing: true,
|
||||||
|
);
|
||||||
|
static Widget idle() => const FaiStatusDot(
|
||||||
|
color: FaiColors.muted,
|
||||||
|
);
|
||||||
|
static Widget down() => const FaiStatusDot(
|
||||||
|
color: FaiColors.danger,
|
||||||
|
);
|
||||||
|
}
|
||||||
12
lib/widgets/widgets.dart
Normal file
12
lib/widgets/widgets.dart
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Barrel file for the F∆I Studio widget library.
|
||||||
|
//
|
||||||
|
// Pages should `import '../widgets/widgets.dart';` and use the
|
||||||
|
// primitives directly — no Material `Card` / generic `Container`
|
||||||
|
// soup in page code.
|
||||||
|
|
||||||
|
export 'fai_card.dart';
|
||||||
|
export 'fai_data_row.dart';
|
||||||
|
export 'fai_delta_mark.dart';
|
||||||
|
export 'fai_empty_state.dart';
|
||||||
|
export 'fai_pill.dart';
|
||||||
|
export 'fai_status_dot.dart';
|
||||||
|
|
@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
jni
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||||
|
|
|
||||||
42
macos/Podfile
Normal file
42
macos/Podfile
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
platform :osx, '10.15'
|
||||||
|
|
||||||
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
project 'Runner', {
|
||||||
|
'Debug' => :debug,
|
||||||
|
'Profile' => :release,
|
||||||
|
'Release' => :release,
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutter_root
|
||||||
|
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
|
||||||
|
unless File.exist?(generated_xcode_build_settings_path)
|
||||||
|
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||||
|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||||
|
return matches[1].strip if matches
|
||||||
|
end
|
||||||
|
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
|
||||||
|
end
|
||||||
|
|
||||||
|
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||||
|
|
||||||
|
flutter_macos_podfile_setup
|
||||||
|
|
||||||
|
target 'Runner' do
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
|
||||||
|
target 'RunnerTests' do
|
||||||
|
inherit! :search_paths
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
flutter_additional_macos_build_settings(target)
|
||||||
|
end
|
||||||
|
end
|
||||||
16
macos/Podfile.lock
Normal file
16
macos/Podfile.lock
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
PODS:
|
||||||
|
- FlutterMacOS (1.0.0)
|
||||||
|
|
||||||
|
DEPENDENCIES:
|
||||||
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
|
|
||||||
|
EXTERNAL SOURCES:
|
||||||
|
FlutterMacOS:
|
||||||
|
:path: Flutter/ephemeral
|
||||||
|
|
||||||
|
SPEC CHECKSUMS:
|
||||||
|
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||||
|
|
||||||
|
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
||||||
|
|
||||||
|
COCOAPODS: 1.16.2
|
||||||
|
|
@ -27,6 +27,8 @@
|
||||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||||
|
370E355F35F0FAEC7D04F94B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D6BC1A8D7E37B926CF5933E /* Pods_Runner.framework */; };
|
||||||
|
E1309159C1F5025EF6B9821D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 770B980D8F15B767C49B23E5 /* Pods_RunnerTests.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
|
@ -60,11 +62,12 @@
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
1C93D4763670B5002EC24A25 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||||
33CC10ED2044A3C60003C045 /* fai_studio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "fai_studio.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
33CC10ED2044A3C60003C045 /* fai_studio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = fai_studio.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||||
|
|
@ -76,8 +79,15 @@
|
||||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||||
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||||
|
5D6BC1A8D7E37B926CF5933E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
770B980D8F15B767C49B23E5 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
824F23C5F5879DEACA815F19 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
DD47B0891752DBBA76ABCFB5 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
E22C4A447E85951E27417C91 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
F7EFCA08C84A46B6188E5963 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
FB500439F345FA03D73E5AB2 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
|
@ -85,6 +95,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
E1309159C1F5025EF6B9821D /* Pods_RunnerTests.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
@ -92,6 +103,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
370E355F35F0FAEC7D04F94B /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
@ -125,6 +137,7 @@
|
||||||
331C80D6294CF71000263BE5 /* RunnerTests */,
|
331C80D6294CF71000263BE5 /* RunnerTests */,
|
||||||
33CC10EE2044A3C60003C045 /* Products */,
|
33CC10EE2044A3C60003C045 /* Products */,
|
||||||
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
||||||
|
4C343B8693BC75A616AA05F5 /* Pods */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
|
@ -172,9 +185,25 @@
|
||||||
path = Runner;
|
path = Runner;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
4C343B8693BC75A616AA05F5 /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
F7EFCA08C84A46B6188E5963 /* Pods-Runner.debug.xcconfig */,
|
||||||
|
E22C4A447E85951E27417C91 /* Pods-Runner.release.xcconfig */,
|
||||||
|
FB500439F345FA03D73E5AB2 /* Pods-Runner.profile.xcconfig */,
|
||||||
|
DD47B0891752DBBA76ABCFB5 /* Pods-RunnerTests.debug.xcconfig */,
|
||||||
|
1C93D4763670B5002EC24A25 /* Pods-RunnerTests.release.xcconfig */,
|
||||||
|
824F23C5F5879DEACA815F19 /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Pods;
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
5D6BC1A8D7E37B926CF5933E /* Pods_Runner.framework */,
|
||||||
|
770B980D8F15B767C49B23E5 /* Pods_RunnerTests.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -186,6 +215,7 @@
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
51FEB1BF98FD7036ADA63936 /* [CP] Check Pods Manifest.lock */,
|
||||||
331C80D1294CF70F00263BE5 /* Sources */,
|
331C80D1294CF70F00263BE5 /* Sources */,
|
||||||
331C80D2294CF70F00263BE5 /* Frameworks */,
|
331C80D2294CF70F00263BE5 /* Frameworks */,
|
||||||
331C80D3294CF70F00263BE5 /* Resources */,
|
331C80D3294CF70F00263BE5 /* Resources */,
|
||||||
|
|
@ -204,6 +234,7 @@
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
546C72FC3468AB7FBBCFD16E /* [CP] Check Pods Manifest.lock */,
|
||||||
33CC10E92044A3C60003C045 /* Sources */,
|
33CC10E92044A3C60003C045 /* Sources */,
|
||||||
33CC10EA2044A3C60003C045 /* Frameworks */,
|
33CC10EA2044A3C60003C045 /* Frameworks */,
|
||||||
33CC10EB2044A3C60003C045 /* Resources */,
|
33CC10EB2044A3C60003C045 /* Resources */,
|
||||||
|
|
@ -329,6 +360,50 @@
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
||||||
};
|
};
|
||||||
|
51FEB1BF98FD7036ADA63936 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
546C72FC3468AB7FBBCFD16E /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
|
@ -380,6 +455,7 @@
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
331C80DB294CF71000263BE5 /* Debug */ = {
|
331C80DB294CF71000263BE5 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = DD47B0891752DBBA76ABCFB5 /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
|
@ -394,6 +470,7 @@
|
||||||
};
|
};
|
||||||
331C80DC294CF71000263BE5 /* Release */ = {
|
331C80DC294CF71000263BE5 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 1C93D4763670B5002EC24A25 /* Pods-RunnerTests.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
|
@ -408,6 +485,7 @@
|
||||||
};
|
};
|
||||||
331C80DD294CF71000263BE5 /* Profile */ = {
|
331C80DD294CF71000263BE5 /* Profile */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 824F23C5F5879DEACA815F19 /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,7 @@
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "group:Runner.xcodeproj">
|
location = "group:Runner.xcodeproj">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
</Workspace>
|
</Workspace>
|
||||||
|
|
|
||||||
202
pubspec.lock
202
pubspec.lock
|
|
@ -1,6 +1,14 @@
|
||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -33,6 +41,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
|
code_assets:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: code_assets
|
||||||
|
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -72,6 +88,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.3"
|
version: "1.3.3"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -98,6 +130,14 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
glob:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: glob
|
||||||
|
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
google_cloud:
|
google_cloud:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -106,6 +146,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.1"
|
version: "0.4.1"
|
||||||
|
google_fonts:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: google_fonts
|
||||||
|
sha256: "4e9391085e524954a51e3625b7c9c7e9851dc3f376603208bb45c24b9a66255d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.1.0"
|
||||||
google_identity_services_web:
|
google_identity_services_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -130,6 +178,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.0"
|
version: "4.2.0"
|
||||||
|
hooks:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hooks
|
||||||
|
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -162,6 +218,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "1.0.5"
|
||||||
|
jni:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: jni
|
||||||
|
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
jni_flutter:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: jni_flutter
|
||||||
|
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -194,6 +266,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.0"
|
version: "6.1.0"
|
||||||
|
logging:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -218,6 +298,30 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.17.0"
|
||||||
|
native_toolchain_c:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: native_toolchain_c
|
||||||
|
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.17.6"
|
||||||
|
objective_c:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: objective_c
|
||||||
|
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.3.0"
|
||||||
|
package_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_config
|
||||||
|
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -226,6 +330,70 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
|
path_provider:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.5"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.1"
|
||||||
|
path_provider_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_foundation
|
||||||
|
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.0"
|
||||||
|
path_provider_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_linux
|
||||||
|
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.1"
|
||||||
|
path_provider_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_platform_interface
|
||||||
|
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
path_provider_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_windows
|
||||||
|
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
|
platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.6"
|
||||||
|
plugin_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: plugin_platform_interface
|
||||||
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
protobuf:
|
protobuf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -234,6 +402,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.0"
|
version: "4.2.0"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
record_use:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_use
|
||||||
|
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
shelf:
|
shelf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -327,6 +511,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
xdg_directories:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xdg_directories
|
||||||
|
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.11.0-200.1.beta <4.0.0"
|
dart: ">=3.11.0-200.1.beta <4.0.0"
|
||||||
flutter: ">=3.18.0-18.0.pre.54"
|
flutter: ">=3.38.4"
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ dependencies:
|
||||||
# Sibling package; will move to Forgejo path once published.
|
# Sibling package; will move to Forgejo path once published.
|
||||||
fai_dart_sdk:
|
fai_dart_sdk:
|
||||||
path: ../fai_dart_sdk
|
path: ../fai_dart_sdk
|
||||||
|
google_fonts: ^8.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
jni
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue