@marcos_feitoza/personal-finance-frontend-core-ui 1.2.0 → 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,17 @@
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
+
8
+ ## [1.2.1](https://github.com/MarcosOps/personal-finance-frontend-core-ui/compare/v1.2.0...v1.2.1) (2026-01-30)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Web responsivo (mobile/tablet/resize) ([dba28a4](https://github.com/MarcosOps/personal-finance-frontend-core-ui/commit/dba28a4c370aac6f02a0e304e25c6eebaca4b961))
14
+
1
15
  # [1.2.0](https://github.com/MarcosOps/personal-finance-frontend-core-ui/compare/v1.1.0...v1.2.0) (2026-01-29)
2
16
 
3
17
 
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,5 +1,7 @@
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';
5
6
  export 'utils/app_snackbars.dart';
7
+ export 'utils/app_responsive.dart';
@@ -10,17 +10,17 @@ class AppDialogs {
10
10
  context: context,
11
11
  builder: (BuildContext context) {
12
12
  return AlertDialog(
13
- title: const Text('Confirmar Exclusão'),
13
+ title: const Text('Confirm Deletion'),
14
14
  content: Text(
15
- 'Você tem certeza que deseja excluir "$itemName"? Esta ação não pode ser desfeita.'),
15
+ 'Are you sure you want to delete "$itemName"? This action cannot be undone.'),
16
16
  actions: <Widget>[
17
17
  TextButton(
18
18
  onPressed: () => Navigator.of(context).pop(false),
19
- child: const Text('Cancelar'),
19
+ child: const Text('Cancel'),
20
20
  ),
21
21
  TextButton(
22
22
  onPressed: () => Navigator.of(context).pop(true),
23
- child: const Text('Excluir'),
23
+ child: const Text('Delete'),
24
24
  ),
25
25
  ],
26
26
  );
@@ -0,0 +1,87 @@
1
+ import 'package:flutter/widgets.dart';
2
+
3
+ /// Simple responsive helpers and breakpoints.
4
+ ///
5
+ /// Why this exists:
6
+ /// - Most of the app was built desktop-first (many wide `Row`s and `DataTable`s).
7
+ /// - On mobile/tablet or when resizing the browser, layouts overflow.
8
+ /// - These helpers give us one place to define breakpoints and common patterns.
9
+ ///
10
+ /// Breakpoints (can be adjusted later):
11
+ /// - mobile: < 600
12
+ /// - tablet: 600 .. < 1024
13
+ /// - desktop: >= 1024
14
+ class AppBreakpoints {
15
+ AppBreakpoints._();
16
+
17
+ static const double mobile = 600;
18
+ static const double tablet = 1024;
19
+
20
+ static bool isMobileWidth(double width) => width < mobile;
21
+ static bool isTabletWidth(double width) => width >= mobile && width < tablet;
22
+ static bool isDesktopWidth(double width) => width >= tablet;
23
+ }
24
+
25
+ /// Builds different widgets depending on the current available width.
26
+ class ResponsiveBuilder extends StatelessWidget {
27
+ final Widget Function(BuildContext context) mobile;
28
+ final Widget Function(BuildContext context)? tablet;
29
+ final Widget Function(BuildContext context) desktop;
30
+
31
+ const ResponsiveBuilder({
32
+ super.key,
33
+ required this.mobile,
34
+ this.tablet,
35
+ required this.desktop,
36
+ });
37
+
38
+ @override
39
+ Widget build(BuildContext context) {
40
+ return LayoutBuilder(
41
+ builder: (context, constraints) {
42
+ final width = constraints.maxWidth;
43
+
44
+ if (AppBreakpoints.isMobileWidth(width)) {
45
+ return mobile(context);
46
+ }
47
+
48
+ if (AppBreakpoints.isTabletWidth(width)) {
49
+ return (tablet ?? desktop)(context);
50
+ }
51
+
52
+ return desktop(context);
53
+ },
54
+ );
55
+ }
56
+ }
57
+
58
+ /// Wraps a wide widget (like DataTable) with horizontal scroll when needed.
59
+ ///
60
+ /// On desktop the widget is rendered normally.
61
+ /// On mobile/tablet we enable horizontal scrolling to avoid overflow.
62
+ class ResponsiveHorizontalScroll extends StatelessWidget {
63
+ final Widget child;
64
+
65
+ const ResponsiveHorizontalScroll({super.key, required this.child});
66
+
67
+ @override
68
+ Widget build(BuildContext context) {
69
+ return LayoutBuilder(
70
+ builder: (context, constraints) {
71
+ final width = constraints.maxWidth;
72
+ final needsScroll = !AppBreakpoints.isDesktopWidth(width);
73
+
74
+ if (!needsScroll) return child;
75
+
76
+ return SingleChildScrollView(
77
+ scrollDirection: Axis.horizontal,
78
+ child: ConstrainedBox(
79
+ // Make sure the child has at least the current width.
80
+ constraints: BoxConstraints(minWidth: width),
81
+ child: child,
82
+ ),
83
+ );
84
+ },
85
+ );
86
+ }
87
+ }
@@ -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
+ }
@@ -1,4 +1,5 @@
1
1
  import 'package:flutter/material.dart';
2
+ import 'package:personal_finance_frontend_core_ui/utils/app_responsive.dart';
2
3
 
3
4
  /// A reusable widget to display historical data in a standardized DataTable.
4
5
  class HistoricalDataTable extends StatelessWidget {
@@ -24,8 +25,7 @@ class HistoricalDataTable extends StatelessWidget {
24
25
  );
25
26
  }
26
27
 
27
- return SingleChildScrollView(
28
- scrollDirection: Axis.horizontal,
28
+ return ResponsiveHorizontalScroll(
29
29
  child: DataTable(
30
30
  columnSpacing: 24.0,
31
31
  columns: columns,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcos_feitoza/personal-finance-frontend-core-ui",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },