@marcos_feitoza/personal-finance-frontend-core-ui 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [1.2.2](https://github.com/MarcosOps/personal-finance-frontend-core-ui/compare/v1.2.1...v1.2.2) (2026-01-30)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Padronizar mensagens de erro e estados de loading ([66ea1d0](https://github.com/MarcosOps/personal-finance-frontend-core-ui/commit/66ea1d074dffa12bcd068265a6920e5997c20f2b))
7
+
1
8
  ## [1.2.1](https://github.com/MarcosOps/personal-finance-frontend-core-ui/compare/v1.2.0...v1.2.1) (2026-01-30)
2
9
 
3
10
 
package/README.md CHANGED
@@ -75,6 +75,34 @@ showErrorSnackBar(context, 'Something went wrong');
75
75
 
76
76
  ---
77
77
 
78
+ ## Loading / Empty / Error / Success (Fase 3)
79
+
80
+ Para evitar telas “silenciosas” e condicionais repetidos em cada screen (`if (isLoading) ... else if (error) ...`), use o widget centralizado:
81
+
82
+ - `lib/widgets/app_async_state_view.dart`
83
+
84
+ Ele padroniza os estados:
85
+
86
+ - **loading**: `CircularProgressIndicator`
87
+ - **error**: mensagem + botão “Try again” (opcional)
88
+ - **empty**: mensagem + ação (opcional)
89
+ - **success**: renderiza o `child`
90
+
91
+ Exemplo:
92
+
93
+ ```dart
94
+ return AppAsyncStateView(
95
+ isLoading: viewModel.isLoading,
96
+ errorMessage: viewModel.errorMessage,
97
+ isEmpty: viewModel.items.isEmpty,
98
+ emptyMessage: 'No items found.',
99
+ onRetry: viewModel.fetch,
100
+ child: ListView(...),
101
+ );
102
+ ```
103
+
104
+ ---
105
+
78
106
  ## Como Usar (Instalação como Dependência)
79
107
 
80
108
  Este pacote é uma dependência local para a aplicação principal (`personal-finance-frontend`) e outras features packages do frontend.
@@ -1,4 +1,5 @@
1
1
  export 'widgets/app_widgets.dart';
2
+ export 'widgets/app_async_state_view.dart';
2
3
  export 'theme/app_theme.dart';
3
4
  export 'theme/app_colors.dart';
4
5
  export 'theme/app_spacing.dart';
@@ -0,0 +1,147 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ /// A centralized widget to standardize async UI states.
4
+ ///
5
+ /// Goal: avoid duplicated `if (isLoading) ... else if (error) ...` blocks
6
+ /// across screens and keep messaging consistent.
7
+ ///
8
+ /// Supported states:
9
+ /// - loading: shows a centered progress indicator
10
+ /// - error: shows a message + optional retry button
11
+ /// - empty: shows a message + optional action
12
+ /// - success: renders the provided [child]
13
+ ///
14
+ /// Usage:
15
+ /// ```dart
16
+ /// return AppAsyncStateView(
17
+ /// isLoading: viewModel.isLoading,
18
+ /// errorMessage: viewModel.errorMessage,
19
+ /// isEmpty: viewModel.items.isEmpty,
20
+ /// emptyMessage: 'No items found.',
21
+ /// onRetry: viewModel.fetch,
22
+ /// child: ListView(...),
23
+ /// );
24
+ /// ```
25
+ class AppAsyncStateView extends StatelessWidget {
26
+ final bool isLoading;
27
+
28
+ /// Any non-empty string will be treated as an error state.
29
+ final String? errorMessage;
30
+
31
+ /// The screen decides what "empty" means (list empty, object null, etc.).
32
+ final bool isEmpty;
33
+ final String emptyMessage;
34
+
35
+ /// Optional retry action, commonly used for network errors.
36
+ final VoidCallback? onRetry;
37
+
38
+ /// Optional action shown in the empty state.
39
+ final String? emptyActionLabel;
40
+ final VoidCallback? onEmptyAction;
41
+
42
+ /// The content when success.
43
+ final Widget child;
44
+
45
+ /// You can override the default loading indicator.
46
+ final Widget? loadingWidget;
47
+
48
+ const AppAsyncStateView({
49
+ super.key,
50
+ required this.isLoading,
51
+ required this.child,
52
+ this.errorMessage,
53
+ this.isEmpty = false,
54
+ this.emptyMessage = 'No data found.',
55
+ this.onRetry,
56
+ this.emptyActionLabel,
57
+ this.onEmptyAction,
58
+ this.loadingWidget,
59
+ });
60
+
61
+ bool get _hasError => (errorMessage ?? '').trim().isNotEmpty;
62
+
63
+ @override
64
+ Widget build(BuildContext context) {
65
+ if (isLoading) {
66
+ return loadingWidget ?? const Center(child: CircularProgressIndicator());
67
+ }
68
+
69
+ if (_hasError) {
70
+ return _CenteredMessage(
71
+ title: 'Something went wrong',
72
+ message: errorMessage!.trim(),
73
+ icon: Icons.error_outline,
74
+ actionLabel: onRetry == null ? null : 'Try again',
75
+ onAction: onRetry,
76
+ );
77
+ }
78
+
79
+ if (isEmpty) {
80
+ return _CenteredMessage(
81
+ title: 'Nothing here yet',
82
+ message: emptyMessage,
83
+ icon: Icons.inbox_outlined,
84
+ actionLabel: emptyActionLabel,
85
+ onAction: onEmptyAction,
86
+ );
87
+ }
88
+
89
+ return child;
90
+ }
91
+ }
92
+
93
+ class _CenteredMessage extends StatelessWidget {
94
+ final String title;
95
+ final String message;
96
+ final IconData icon;
97
+ final String? actionLabel;
98
+ final VoidCallback? onAction;
99
+
100
+ const _CenteredMessage({
101
+ required this.title,
102
+ required this.message,
103
+ required this.icon,
104
+ this.actionLabel,
105
+ this.onAction,
106
+ });
107
+
108
+ @override
109
+ Widget build(BuildContext context) {
110
+ final theme = Theme.of(context);
111
+ return Center(
112
+ child: Padding(
113
+ padding: const EdgeInsets.all(24.0),
114
+ child: ConstrainedBox(
115
+ constraints: const BoxConstraints(maxWidth: 480),
116
+ child: Column(
117
+ mainAxisSize: MainAxisSize.min,
118
+ children: [
119
+ Icon(icon, size: 44, color: theme.colorScheme.onSurfaceVariant),
120
+ const SizedBox(height: 12),
121
+ Text(
122
+ title,
123
+ textAlign: TextAlign.center,
124
+ style: theme.textTheme.titleMedium,
125
+ ),
126
+ const SizedBox(height: 8),
127
+ Text(
128
+ message,
129
+ textAlign: TextAlign.center,
130
+ style: theme.textTheme.bodyMedium?.copyWith(
131
+ color: theme.colorScheme.onSurfaceVariant,
132
+ ),
133
+ ),
134
+ if (actionLabel != null && onAction != null) ...[
135
+ const SizedBox(height: 16),
136
+ ElevatedButton(
137
+ onPressed: onAction,
138
+ child: Text(actionLabel!),
139
+ ),
140
+ ],
141
+ ],
142
+ ),
143
+ ),
144
+ ),
145
+ );
146
+ }
147
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcos_feitoza/personal-finance-frontend-core-ui",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },