diff --git a/useragent/lib/providers/evm/evm_grants.dart b/useragent/lib/providers/evm/evm_grants.dart index 6d7747e..fb03342 100644 --- a/useragent/lib/providers/evm/evm_grants.dart +++ b/useragent/lib/providers/evm/evm_grants.dart @@ -1,11 +1,9 @@ import 'package:arbiter/features/connection/evm/grants.dart'; import 'package:arbiter/proto/evm.pb.dart'; import 'package:arbiter/providers/connection/connection_manager.dart'; -import 'package:fixnum/fixnum.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/experimental/mutation.dart'; import 'package:mtcore/markettakers.dart'; -import 'package:protobuf/well_known_types/google/protobuf/timestamp.pb.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'evm_grants.freezed.dart'; diff --git a/useragent/lib/screens/callouts/sdk_connect.dart b/useragent/lib/screens/callouts/sdk_connect.dart index c26fd36..3e005eb 100644 --- a/useragent/lib/screens/callouts/sdk_connect.dart +++ b/useragent/lib/screens/callouts/sdk_connect.dart @@ -1,5 +1,6 @@ import 'package:arbiter/proto/client.pb.dart'; import 'package:arbiter/theme/palette.dart'; +import 'package:arbiter/widgets/cream_frame.dart'; import 'package:flutter/material.dart'; import 'package:sizer/sizer.dart'; @@ -31,12 +32,7 @@ class SdkConnectCallout extends StatelessWidget { clientInfo.hasVersion() && clientInfo.version.isNotEmpty; final showInfoCard = hasDescription || hasVersion; - return Container( - decoration: BoxDecoration( - color: Palette.cream, - borderRadius: BorderRadius.circular(24), - border: Border.all(color: Palette.line), - ), + return CreamFrame( padding: EdgeInsets.all(2.4.h), child: Column( mainAxisSize: MainAxisSize.min, diff --git a/useragent/lib/screens/dashboard/clients/details/widgets/client_details_state_panel.dart b/useragent/lib/screens/dashboard/clients/details/widgets/client_details_state_panel.dart index f9c40d5..82f5b41 100644 --- a/useragent/lib/screens/dashboard/clients/details/widgets/client_details_state_panel.dart +++ b/useragent/lib/screens/dashboard/clients/details/widgets/client_details_state_panel.dart @@ -1,4 +1,5 @@ import 'package:arbiter/theme/palette.dart'; +import 'package:arbiter/widgets/cream_frame.dart'; import 'package:flutter/material.dart'; class ClientDetailsStatePanel extends StatelessWidget { @@ -17,27 +18,18 @@ class ClientDetailsStatePanel extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); return Center( - child: Padding( + child: CreamFrame( + margin: const EdgeInsets.all(24), padding: const EdgeInsets.all(24), - child: DecoratedBox( - decoration: BoxDecoration( - color: Palette.cream, - borderRadius: BorderRadius.circular(24), - border: Border.all(color: Palette.line), - ), - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, color: Palette.coral), - const SizedBox(height: 12), - Text(title, style: theme.textTheme.titleLarge), - const SizedBox(height: 8), - Text(body, textAlign: TextAlign.center), - ], - ), - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: Palette.coral), + const SizedBox(height: 12), + Text(title, style: theme.textTheme.titleLarge), + const SizedBox(height: 8), + Text(body, textAlign: TextAlign.center), + ], ), ), ); diff --git a/useragent/lib/screens/dashboard/clients/details/widgets/client_summary_card.dart b/useragent/lib/screens/dashboard/clients/details/widgets/client_summary_card.dart index 7fa081c..f04576d 100644 --- a/useragent/lib/screens/dashboard/clients/details/widgets/client_summary_card.dart +++ b/useragent/lib/screens/dashboard/clients/details/widgets/client_summary_card.dart @@ -1,5 +1,5 @@ import 'package:arbiter/proto/user_agent.pb.dart'; -import 'package:arbiter/theme/palette.dart'; +import 'package:arbiter/widgets/cream_frame.dart'; import 'package:flutter/material.dart'; class ClientSummaryCard extends StatelessWidget { @@ -9,15 +9,9 @@ class ClientSummaryCard extends StatelessWidget { @override Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - color: Palette.cream, - borderRadius: BorderRadius.circular(24), - border: Border.all(color: Palette.line), - ), - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( + return CreamFrame( + padding: const EdgeInsets.all(20), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( @@ -42,7 +36,6 @@ class ClientSummaryCard extends StatelessWidget { ), ], ), - ), ); } } diff --git a/useragent/lib/screens/dashboard/clients/details/widgets/wallet_access_save_bar.dart b/useragent/lib/screens/dashboard/clients/details/widgets/wallet_access_save_bar.dart index 52e820d..b96f2f6 100644 --- a/useragent/lib/screens/dashboard/clients/details/widgets/wallet_access_save_bar.dart +++ b/useragent/lib/screens/dashboard/clients/details/widgets/wallet_access_save_bar.dart @@ -1,5 +1,6 @@ import 'package:arbiter/providers/sdk_clients/wallet_access.dart'; import 'package:arbiter/theme/palette.dart'; +import 'package:arbiter/widgets/cream_frame.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/experimental/mutation.dart'; @@ -24,15 +25,9 @@ class WalletAccessSaveBar extends StatelessWidget { MutationError(:final error) => error.toString(), _ => null, }; - return DecoratedBox( - decoration: BoxDecoration( - color: Palette.cream, - borderRadius: BorderRadius.circular(24), - border: Border.all(color: Palette.line), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( + return CreamFrame( + padding: const EdgeInsets.all(16), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (errorText != null) ...[ @@ -54,7 +49,6 @@ class WalletAccessSaveBar extends StatelessWidget { ), ], ), - ), ); } } diff --git a/useragent/lib/screens/dashboard/clients/details/widgets/wallet_access_section.dart b/useragent/lib/screens/dashboard/clients/details/widgets/wallet_access_section.dart index e5b40f2..cf55b29 100644 --- a/useragent/lib/screens/dashboard/clients/details/widgets/wallet_access_section.dart +++ b/useragent/lib/screens/dashboard/clients/details/widgets/wallet_access_section.dart @@ -2,7 +2,7 @@ import 'package:arbiter/providers/sdk_clients/wallet_access.dart'; import 'package:arbiter/screens/dashboard/clients/details/widgets/client_details_state_panel.dart'; import 'package:arbiter/screens/dashboard/clients/details/widgets/wallet_access_list.dart'; import 'package:arbiter/screens/dashboard/clients/details/widgets/wallet_access_search_field.dart'; -import 'package:arbiter/theme/palette.dart'; +import 'package:arbiter/widgets/cream_frame.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -27,15 +27,9 @@ class WalletAccessSection extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final optionsAsync = ref.watch(clientWalletOptionsProvider); - return DecoratedBox( - decoration: BoxDecoration( - color: Palette.cream, - borderRadius: BorderRadius.circular(24), - border: Border.all(color: Palette.line), - ), - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( + return CreamFrame( + padding: const EdgeInsets.all(20), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( @@ -56,7 +50,6 @@ class WalletAccessSection extends ConsumerWidget { ), ], ), - ), ); } } diff --git a/useragent/lib/screens/dashboard/clients/table.dart b/useragent/lib/screens/dashboard/clients/table.dart index a84cfe9..7bfd43c 100644 --- a/useragent/lib/screens/dashboard/clients/table.dart +++ b/useragent/lib/screens/dashboard/clients/table.dart @@ -10,6 +10,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:arbiter/theme/palette.dart'; +import 'package:arbiter/widgets/cream_frame.dart'; +import 'package:arbiter/widgets/state_panel.dart'; import 'package:sizer/sizer.dart'; // ─── Column width getters ───────────────────────────────────────────────────── @@ -59,79 +61,6 @@ String _formatError(Object error) { return message; } -// ─── State panel ───────────────────────────────────────────────────────────── - -class _StatePanel extends StatelessWidget { - const _StatePanel({ - required this.icon, - required this.title, - required this.body, - this.actionLabel, - this.onAction, - this.busy = false, - }); - - final IconData icon; - final String title; - final String body; - final String? actionLabel; - final Future Function()? onAction; - final bool busy; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Palette.cream.withValues(alpha: 0.92), - border: Border.all(color: Palette.line), - ), - child: Padding( - padding: EdgeInsets.all(2.8.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (busy) - SizedBox( - width: 2.8.h, - height: 2.8.h, - child: const CircularProgressIndicator(strokeWidth: 2.5), - ) - else - Icon(icon, size: 34, color: Palette.coral), - SizedBox(height: 1.8.h), - Text( - title, - style: theme.textTheme.headlineSmall?.copyWith( - color: Palette.ink, - fontWeight: FontWeight.w800, - ), - ), - SizedBox(height: 1.h), - Text( - body, - style: theme.textTheme.bodyLarge?.copyWith( - color: Palette.ink.withValues(alpha: 0.72), - height: 1.5, - ), - ), - if (actionLabel != null && onAction != null) ...[ - SizedBox(height: 2.h), - OutlinedButton.icon( - onPressed: () => onAction!(), - icon: const Icon(Icons.refresh), - label: Text(actionLabel!), - ), - ], - ], - ), - ), - ); - } -} - // ─── Header ─────────────────────────────────────────────────────────────────── class _Header extends StatelessWidget { @@ -443,17 +372,11 @@ class _ClientTable extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Palette.cream.withValues(alpha: 0.92), - border: Border.all(color: Palette.line), - ), - child: Padding( - padding: EdgeInsets.all(2.h), - child: LayoutBuilder( - builder: (context, constraints) { - final tableWidth = math.max(_tableMinWidth, constraints.maxWidth); + return CreamFrame( + padding: EdgeInsets.all(2.h), + child: LayoutBuilder( + builder: (context, constraints) { + final tableWidth = math.max(_tableMinWidth, constraints.maxWidth); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -497,7 +420,6 @@ class _ClientTable extends StatelessWidget { ); }, ), - ), ); } } @@ -533,27 +455,27 @@ class ClientsScreen extends HookConsumerWidget { final clients = clientsAsync.asData?.value; final content = switch (clientsAsync) { - AsyncLoading() when clients == null => const _StatePanel( + AsyncLoading() when clients == null => const StatePanel( icon: Icons.hourglass_top, title: 'Loading clients', body: 'Pulling client registry from Arbiter.', busy: true, ), - AsyncError(:final error) => _StatePanel( + AsyncError(:final error) => StatePanel( icon: Icons.sync_problem, title: 'Client registry unavailable', body: _formatError(error), actionLabel: 'Retry', onAction: refresh, ), - _ when !isConnected => _StatePanel( + _ when !isConnected => StatePanel( icon: Icons.portable_wifi_off, title: 'No active server connection', body: 'Reconnect to Arbiter to list SDK clients.', actionLabel: 'Refresh', onAction: refresh, ), - _ when clients != null && clients.isEmpty => _StatePanel( + _ when clients != null && clients.isEmpty => StatePanel( icon: Icons.devices_other_outlined, title: 'No clients yet', body: 'SDK clients appear here once they register with Arbiter.', diff --git a/useragent/lib/screens/dashboard/evm/evm.dart b/useragent/lib/screens/dashboard/evm/evm.dart index 743b369..89bab30 100644 --- a/useragent/lib/screens/dashboard/evm/evm.dart +++ b/useragent/lib/screens/dashboard/evm/evm.dart @@ -4,6 +4,7 @@ import 'package:arbiter/screens/dashboard/evm/wallets/table.dart'; import 'package:arbiter/theme/palette.dart'; import 'package:arbiter/providers/evm/evm.dart'; import 'package:arbiter/widgets/page_header.dart'; +import 'package:arbiter/widgets/state_panel.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -36,20 +37,20 @@ class EvmScreen extends HookConsumerWidget { } final content = switch (evm) { - AsyncLoading() when wallets == null => const _StatePanel( + AsyncLoading() when wallets == null => const StatePanel( icon: Icons.hourglass_top, title: 'Loading wallets', body: 'Pulling wallet registry from Arbiter.', busy: true, ), - AsyncError(:final error) => _StatePanel( + AsyncError(:final error) => StatePanel( icon: Icons.sync_problem, title: 'Wallet registry unavailable', body: _formatError(error), actionLabel: 'Retry', onAction: refreshWallets, ), - AsyncData(:final value) when value == null => _StatePanel( + AsyncData(:final value) when value == null => StatePanel( icon: Icons.portable_wifi_off, title: 'No active server connection', body: 'Reconnect to Arbiter to list or create EVM wallets.', @@ -90,77 +91,6 @@ class EvmScreen extends HookConsumerWidget { } } -class _StatePanel extends StatelessWidget { - const _StatePanel({ - required this.icon, - required this.title, - required this.body, - this.actionLabel, - this.onAction, - this.busy = false, - }); - - final IconData icon; - final String title; - final String body; - final String? actionLabel; - final Future Function()? onAction; - final bool busy; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Palette.cream.withValues(alpha: 0.92), - border: Border.all(color: Palette.line), - ), - child: Padding( - padding: EdgeInsets.all(2.8.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (busy) - SizedBox( - width: 2.8.h, - height: 2.8.h, - child: CircularProgressIndicator(strokeWidth: 2.5), - ) - else - Icon(icon, size: 34, color: Palette.coral), - SizedBox(height: 1.8.h), - Text( - title, - style: theme.textTheme.headlineSmall?.copyWith( - color: Palette.ink, - fontWeight: FontWeight.w800, - ), - ), - SizedBox(height: 1.h), - Text( - body, - style: theme.textTheme.bodyLarge?.copyWith( - color: Palette.ink.withValues(alpha: 0.72), - height: 1.5, - ), - ), - if (actionLabel != null && onAction != null) ...[ - SizedBox(height: 2.h), - OutlinedButton.icon( - onPressed: () => onAction!(), - icon: const Icon(Icons.refresh), - label: Text(actionLabel!), - ), - ], - ], - ), - ), - ); - } -} - String _formatError(Object error) { final message = error.toString(); if (message.startsWith('Exception: ')) { diff --git a/useragent/lib/screens/dashboard/evm/grants/grants.dart b/useragent/lib/screens/dashboard/evm/grants/grants.dart index eb73efc..b3b314b 100644 --- a/useragent/lib/screens/dashboard/evm/grants/grants.dart +++ b/useragent/lib/screens/dashboard/evm/grants/grants.dart @@ -5,6 +5,7 @@ import 'package:arbiter/router.gr.dart'; import 'package:arbiter/screens/dashboard/evm/grants/widgets/grant_card.dart'; import 'package:arbiter/theme/palette.dart'; import 'package:arbiter/widgets/page_header.dart'; +import 'package:arbiter/widgets/state_panel.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -18,79 +19,6 @@ String _formatError(Object error) { return message; } -// ─── State panel ────────────────────────────────────────────────────────────── - -class _StatePanel extends StatelessWidget { - const _StatePanel({ - required this.icon, - required this.title, - required this.body, - this.actionLabel, - this.onAction, - this.busy = false, - }); - - final IconData icon; - final String title; - final String body; - final String? actionLabel; - final Future Function()? onAction; - final bool busy; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Palette.cream.withValues(alpha: 0.92), - border: Border.all(color: Palette.line), - ), - child: Padding( - padding: EdgeInsets.all(2.8.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (busy) - SizedBox( - width: 2.8.h, - height: 2.8.h, - child: const CircularProgressIndicator(strokeWidth: 2.5), - ) - else - Icon(icon, size: 34, color: Palette.coral), - SizedBox(height: 1.8.h), - Text( - title, - style: theme.textTheme.headlineSmall?.copyWith( - color: Palette.ink, - fontWeight: FontWeight.w800, - ), - ), - SizedBox(height: 1.h), - Text( - body, - style: theme.textTheme.bodyLarge?.copyWith( - color: Palette.ink.withValues(alpha: 0.72), - height: 1.5, - ), - ), - if (actionLabel != null && onAction != null) ...[ - SizedBox(height: 2.h), - OutlinedButton.icon( - onPressed: () => onAction!(), - icon: const Icon(Icons.refresh), - label: Text(actionLabel!), - ), - ], - ], - ), - ), - ); - } -} - // ─── Grant list ─────────────────────────────────────────────────────────────── class _GrantList extends StatelessWidget { @@ -149,27 +77,27 @@ class EvmGrantsScreen extends ConsumerWidget { final grants = grantsState?.grants; final content = switch (grantsAsync) { - AsyncLoading() when grantsState == null => const _StatePanel( + AsyncLoading() when grantsState == null => const StatePanel( icon: Icons.hourglass_top, title: 'Loading grants', body: 'Pulling grant registry from Arbiter.', busy: true, ), - AsyncError(:final error) => _StatePanel( + AsyncError(:final error) => StatePanel( icon: Icons.sync_problem, title: 'Grant registry unavailable', body: _formatError(error), actionLabel: 'Retry', onAction: safeRefresh, ), - AsyncData(:final value) when value == null => _StatePanel( + AsyncData(:final value) when value == null => StatePanel( icon: Icons.portable_wifi_off, title: 'No active server connection', body: 'Reconnect to Arbiter to list EVM grants.', actionLabel: 'Refresh', onAction: safeRefresh, ), - _ when grants != null && grants.isEmpty => _StatePanel( + _ when grants != null && grants.isEmpty => StatePanel( icon: Icons.policy_outlined, title: 'No grants yet', body: 'Create a grant to allow SDK clients to sign transactions.', diff --git a/useragent/lib/screens/dashboard/evm/wallets/table.dart b/useragent/lib/screens/dashboard/evm/wallets/table.dart index 1093dfd..a364d72 100644 --- a/useragent/lib/screens/dashboard/evm/wallets/table.dart +++ b/useragent/lib/screens/dashboard/evm/wallets/table.dart @@ -1,6 +1,7 @@ import 'dart:math' as math; import 'package:arbiter/proto/evm.pb.dart'; import 'package:arbiter/theme/palette.dart'; +import 'package:arbiter/widgets/cream_frame.dart'; import 'package:flutter/material.dart'; import 'package:sizer/sizer.dart'; @@ -32,15 +33,9 @@ class WalletTable extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Palette.cream.withValues(alpha: 0.92), - border: Border.all(color: Palette.line), - ), - child: Padding( - padding: EdgeInsets.all(2.h), - child: LayoutBuilder( + return CreamFrame( + padding: EdgeInsets.all(2.h), + child: LayoutBuilder( builder: (context, constraints) { final tableWidth = math.max(_tableMinWidth, constraints.maxWidth); @@ -89,7 +84,6 @@ class WalletTable extends StatelessWidget { ); }, ), - ), ); } } diff --git a/useragent/lib/widgets/cream_frame.dart b/useragent/lib/widgets/cream_frame.dart new file mode 100644 index 0000000..a4e19f7 --- /dev/null +++ b/useragent/lib/widgets/cream_frame.dart @@ -0,0 +1,32 @@ +import 'package:arbiter/theme/palette.dart'; +import 'package:flutter/material.dart'; + +/// A card-shaped frame with the cream background, rounded corners, and a +/// subtle border. Use [padding] for interior spacing and [margin] for exterior +/// spacing. +class CreamFrame extends StatelessWidget { + const CreamFrame({ + super.key, + required this.child, + this.padding = EdgeInsets.zero, + this.margin, + }); + + final Widget child; + final EdgeInsetsGeometry padding; + final EdgeInsetsGeometry? margin; + + @override + Widget build(BuildContext context) { + return Container( + margin: margin, + padding: padding, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + color: Palette.cream, + border: Border.all(color: Palette.line), + ), + child: child, + ); + } +} diff --git a/useragent/lib/widgets/state_panel.dart b/useragent/lib/widgets/state_panel.dart new file mode 100644 index 0000000..4c73875 --- /dev/null +++ b/useragent/lib/widgets/state_panel.dart @@ -0,0 +1,69 @@ +import 'package:arbiter/widgets/cream_frame.dart'; +import 'package:arbiter/theme/palette.dart'; +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; + +class StatePanel extends StatelessWidget { + const StatePanel({ + super.key, + required this.icon, + required this.title, + required this.body, + this.actionLabel, + this.onAction, + this.busy = false, + }); + + final IconData icon; + final String title; + final String body; + final String? actionLabel; + final Future Function()? onAction; + final bool busy; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return CreamFrame( + padding: EdgeInsets.all(2.8.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (busy) + SizedBox( + width: 2.8.h, + height: 2.8.h, + child: const CircularProgressIndicator(strokeWidth: 2.5), + ) + else + Icon(icon, size: 34, color: Palette.coral), + SizedBox(height: 1.8.h), + Text( + title, + style: theme.textTheme.headlineSmall?.copyWith( + color: Palette.ink, + fontWeight: FontWeight.w800, + ), + ), + SizedBox(height: 1.h), + Text( + body, + style: theme.textTheme.bodyLarge?.copyWith( + color: Palette.ink.withValues(alpha: 0.72), + height: 1.5, + ), + ), + if (actionLabel != null && onAction != null) ...[ + SizedBox(height: 2.h), + OutlinedButton.icon( + onPressed: () => onAction!(), + icon: const Icon(Icons.refresh), + label: Text(actionLabel!), + ), + ], + ], + ), + ); + } +}