diff --git a/.gitignore b/.gitignore
index f2fc72d..5e82815 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,3 +45,9 @@ app.*.map.json
/android/app/release
/android/key.properties
*.jks
+
+# Binários locais de ferramentas (ex.: act)
+/bin/
+
+# Imagens geradas quando um golden falha
+**/failures/
diff --git a/README.md b/README.md
index e956f2f..0fd8d9c 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# RealCalc
-
+
@@ -13,8 +13,6 @@
Uma releitura moderna da **Calculadora do Cidadão**, o app oficial do Banco Central do Brasil para cálculos financeiros. O RealCalc parte da mesma lógica de cálculo (juros compostos, depósitos regulares, financiamento), mas repensa a experiência do zero — identidade visual própria, hierarquia clara entre ações, e uma solução pro problema que o app original nunca resolveu bem: em cada calculadora, qualquer um dos campos pode ser a incógnita, e a interface original não deixa isso óbvio.
> Projeto pessoal e não-oficial. Sem qualquer vínculo com o Banco Central do Brasil — construído como estudo de produto, UX e arquitetura em Flutter.
->
-> Em evolução: algumas rotas já estão previstas na navegação, mas ainda não possuem uma tela implementada.
## O problema que o projeto resolve
@@ -34,15 +32,31 @@ Essa lógica vive num Cubit dedicado por calculadora, reagindo em tempo real a c
- Simulação de depósitos regulares com capitalização composta.
- Fluxo de financiamento com os 4 estados de campo descritos acima.
- Card de métrica com a taxa Selic atual, variação desde o último ajuste do Copom e uma sparkline de tendência — com estados de carregamento e offline (mostra o último valor salvo, ou um estado de "tentar novamente").
-- Interface com design system próprio: paleta de cores, tipografia e componentes centralizados via `ThemeExtension`, sem cor ou estilo hardcoded espalhado pelos widgets.
+- Interface com design system próprio: paleta de cores (`AppColors`, uma `ThemeExtension` com os dois temas), tipografia e ícones centralizados, sem cor ou estilo hardcoded espalhado pelos widgets.
- Testes unitários, de widgets e de módulos (incluindo a fiação de injeção de dependência do `flutter_modular`).
-
-
-
-
-
-
+### Temas claro e escuro
+
+Cada calculadora tem a própria cor, e a resposta aparece em frase no topo da tela. O app segue o tema do sistema, e o botão de sol e lua na tela inicial troca o tema. A escolha fica salva.
+
+
+
+
Claro
+
+
+
+
+
+
+
+
Escuro
+
+
+
+
+
+
+
## Tecnologias
@@ -54,7 +68,7 @@ Essa lógica vive num Cubit dedicado por calculadora, reagindo em tempo real a c
| `Dio` | Cliente HTTP |
| `equatable` | Comparação de estados e objetos |
| `intl` | Formatação de valores e datas |
-| `google_fonts` | Tipografia (Manrope) |
+| Manrope e Bricolage Grotesque | Tipografia empacotada no app para uso offline |
| `mocktail` e `bloc_test` | Testes |
**API de terceiros:** [API do Banco Central do Brasil](https://dadosabertos.bcb.gov.br/), usada como fonte de dados da taxa Selic.
@@ -110,11 +124,11 @@ test/ # Testes organizados por módulo
## Roadmap
-- [ ] Correção de valores por índice (IPCA, IGP-M, poupança)
+- [x] Correção de valores por índice (IPCA, IGP-M, poupança)
- [ ] Comparação SAC vs. Price
- [ ] Simulação de amortização extra
- [ ] Histórico de cálculos
-- [ ] Modo claro
+- [x] Modo claro
## Contribuindo
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 0d29021..7e428b9 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -7,6 +7,12 @@
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
+analyzer:
+ exclude:
+ - build/**
+ - android/**
+ - ios/**
+ - linux/**
include: package:flutter_lints/flutter.yaml
linter:
diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..4fcc831
Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..5338911
Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..c065d18
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..dc383de
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png differ
diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
index cbe3b2f..dae0427 100644
--- a/android/app/src/main/res/drawable-v21/launch_background.xml
+++ b/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -1,13 +1,9 @@
-
-
-
-
-
+
+ android:src="@mipmap/launch_image" />
diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..06b2cb3
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..00f3fb7
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..e958880
Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..0bc048a
Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..37ff290
Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..1c55d3a
Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png differ
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
index 78107ad..dae0427 100644
--- a/android/app/src/main/res/drawable/launch_background.xml
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -1,13 +1,9 @@
-
-
-
-
-
+
+ android:src="@mipmap/launch_image" />
diff --git a/android/app/src/main/res/drawable/launch_image.png b/android/app/src/main/res/drawable/launch_image.png
deleted file mode 100644
index d415e4e..0000000
Binary files a/android/app/src/main/res/drawable/launch_image.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..c61e081
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index bc9e807..89488c2 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/launch_image.png b/android/app/src/main/res/mipmap-hdpi/launch_image.png
new file mode 100644
index 0000000..781f8ef
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/launch_image.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 5a339dd..23180d2 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/launch_image.png b/android/app/src/main/res/mipmap-mdpi/launch_image.png
new file mode 100644
index 0000000..05a99ec
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/launch_image.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 3949a4a..6c2821f 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/launch_image.png b/android/app/src/main/res/mipmap-xhdpi/launch_image.png
new file mode 100644
index 0000000..e1c87ab
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/launch_image.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index f83edda..ccc5474 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/launch_image.png b/android/app/src/main/res/mipmap-xxhdpi/launch_image.png
new file mode 100644
index 0000000..84a1c25
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/launch_image.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 641e87e..299c8e8 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launch_image.png b/android/app/src/main/res/mipmap-xxxhdpi/launch_image.png
new file mode 100644
index 0000000..9f8bc33
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/launch_image.png differ
diff --git a/android/app/src/main/res/play_store_512.png b/android/app/src/main/res/play_store_512.png
index d415e4e..224ab0b 100644
Binary files a/android/app/src/main/res/play_store_512.png and b/android/app/src/main/res/play_store_512.png differ
diff --git a/android/app/src/main/res/values-night-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml
new file mode 100644
index 0000000..96ed68c
--- /dev/null
+++ b/android/app/src/main/res/values-night-v31/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml
new file mode 100644
index 0000000..ee58347
--- /dev/null
+++ b/android/app/src/main/res/values-v31/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..2fbb901
--- /dev/null
+++ b/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #14121F
+ #14121F
+
diff --git a/assets/branding/app_icon.svg b/assets/branding/app_icon.svg
index ddf5840..97b912d 100644
--- a/assets/branding/app_icon.svg
+++ b/assets/branding/app_icon.svg
@@ -1,8 +1,10 @@
-
\ No newline at end of file
+
diff --git a/assets/branding/app_icon_pequeno.svg b/assets/branding/app_icon_pequeno.svg
new file mode 100644
index 0000000..69d8172
--- /dev/null
+++ b/assets/branding/app_icon_pequeno.svg
@@ -0,0 +1,10 @@
+
diff --git a/assets/fonts/Bricolage-Grotesque-OFL.txt b/assets/fonts/Bricolage-Grotesque-OFL.txt
new file mode 100644
index 0000000..a397658
--- /dev/null
+++ b/assets/fonts/Bricolage-Grotesque-OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2022 The Bricolage Grotesque Project Authors (https://github.com/ateliertriay/bricolage)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/assets/fonts/BricolageGrotesque-ExtraBold.ttf b/assets/fonts/BricolageGrotesque-ExtraBold.ttf
new file mode 100644
index 0000000..74950f2
Binary files /dev/null and b/assets/fonts/BricolageGrotesque-ExtraBold.ttf differ
diff --git a/assets/fonts/Manrope-Bold.ttf b/assets/fonts/Manrope-Bold.ttf
new file mode 100644
index 0000000..746764d
Binary files /dev/null and b/assets/fonts/Manrope-Bold.ttf differ
diff --git a/assets/fonts/Manrope-ExtraBold.ttf b/assets/fonts/Manrope-ExtraBold.ttf
new file mode 100644
index 0000000..d324b62
Binary files /dev/null and b/assets/fonts/Manrope-ExtraBold.ttf differ
diff --git a/assets/fonts/Manrope-Medium.ttf b/assets/fonts/Manrope-Medium.ttf
new file mode 100644
index 0000000..2a169e6
Binary files /dev/null and b/assets/fonts/Manrope-Medium.ttf differ
diff --git a/assets/fonts/Manrope-OFL.txt b/assets/fonts/Manrope-OFL.txt
new file mode 100644
index 0000000..e271172
--- /dev/null
+++ b/assets/fonts/Manrope-OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2018 The Manrope Project Authors (https://github.com/googlefonts/manrope)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/assets/fonts/Manrope-SemiBold.ttf b/assets/fonts/Manrope-SemiBold.ttf
new file mode 100644
index 0000000..595e7dc
Binary files /dev/null and b/assets/fonts/Manrope-SemiBold.ttf differ
diff --git a/assets/icons/alert.svg b/assets/icons/alert.svg
new file mode 100644
index 0000000..5a3477c
--- /dev/null
+++ b/assets/icons/alert.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/arrow_down.svg b/assets/icons/arrow_down.svg
new file mode 100644
index 0000000..8d0c5eb
--- /dev/null
+++ b/assets/icons/arrow_down.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/arrow_up.svg b/assets/icons/arrow_up.svg
new file mode 100644
index 0000000..114b5ca
--- /dev/null
+++ b/assets/icons/arrow_up.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/back.svg b/assets/icons/back.svg
new file mode 100644
index 0000000..4034ace
--- /dev/null
+++ b/assets/icons/back.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/bank.svg b/assets/icons/bank.svg
new file mode 100644
index 0000000..ef6b2b1
--- /dev/null
+++ b/assets/icons/bank.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/check.svg b/assets/icons/check.svg
new file mode 100644
index 0000000..195da74
--- /dev/null
+++ b/assets/icons/check.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/chevron_down.svg b/assets/icons/chevron_down.svg
new file mode 100644
index 0000000..4373c8b
--- /dev/null
+++ b/assets/icons/chevron_down.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/chevron_right.svg b/assets/icons/chevron_right.svg
new file mode 100644
index 0000000..4495c34
--- /dev/null
+++ b/assets/icons/chevron_right.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/close.svg b/assets/icons/close.svg
new file mode 100644
index 0000000..c30dc8c
--- /dev/null
+++ b/assets/icons/close.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/cycle.svg b/assets/icons/cycle.svg
new file mode 100644
index 0000000..8d5ed8e
--- /dev/null
+++ b/assets/icons/cycle.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/help.svg b/assets/icons/help.svg
new file mode 100644
index 0000000..1ec8bbf
--- /dev/null
+++ b/assets/icons/help.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/moon.svg b/assets/icons/moon.svg
new file mode 100644
index 0000000..f486300
--- /dev/null
+++ b/assets/icons/moon.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/piggy_bank.svg b/assets/icons/piggy_bank.svg
new file mode 100644
index 0000000..88890b9
--- /dev/null
+++ b/assets/icons/piggy_bank.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/reset.svg b/assets/icons/reset.svg
new file mode 100644
index 0000000..a025005
--- /dev/null
+++ b/assets/icons/reset.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/search.svg b/assets/icons/search.svg
new file mode 100644
index 0000000..8efdea2
--- /dev/null
+++ b/assets/icons/search.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/share.svg b/assets/icons/share.svg
new file mode 100644
index 0000000..ac9b409
--- /dev/null
+++ b/assets/icons/share.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/sun.svg b/assets/icons/sun.svg
new file mode 100644
index 0000000..1cde33d
--- /dev/null
+++ b/assets/icons/sun.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/trending_up.svg b/assets/icons/trending_up.svg
new file mode 100644
index 0000000..77cd100
--- /dev/null
+++ b/assets/icons/trending_up.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/wifi_off.svg b/assets/icons/wifi_off.svg
new file mode 100644
index 0000000..d05c931
--- /dev/null
+++ b/assets/icons/wifi_off.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/launcher/adaptive_foreground_1024.png b/assets/launcher/adaptive_foreground_1024.png
new file mode 100644
index 0000000..9904b69
Binary files /dev/null and b/assets/launcher/adaptive_foreground_1024.png differ
diff --git a/assets/launcher/adaptive_monochrome_1024.png b/assets/launcher/adaptive_monochrome_1024.png
new file mode 100644
index 0000000..c7ef6f8
Binary files /dev/null and b/assets/launcher/adaptive_monochrome_1024.png differ
diff --git a/assets/launcher/icon_1024.png b/assets/launcher/icon_1024.png
new file mode 100644
index 0000000..3f4d650
Binary files /dev/null and b/assets/launcher/icon_1024.png differ
diff --git a/assets/readme/banner.png b/assets/readme/banner.png
index 9056b93..8543838 100644
Binary files a/assets/readme/banner.png and b/assets/readme/banner.png differ
diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 0000000..fa0b357
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1,3 @@
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
+extensions:
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
index d36b1fa..d0d98aa 100644
--- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,122 +1 @@
-{
- "images" : [
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "83.5x83.5",
- "idiom" : "ipad",
- "filename" : "Icon-App-83.5x83.5@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "1024x1024",
- "idiom" : "ios-marketing",
- "filename" : "Icon-App-1024x1024@1x.png",
- "scale" : "1x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
+{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
\ No newline at end of file
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
index dc9ada4..bbfbe0a 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
index 7353c41..4846259 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
index 797d452..b390e08 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
index 6ed2d93..d002e34 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
index 4cd7b00..b89c4db 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
index fe73094..262d09c 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
index 321773c..7961339 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
index 797d452..b390e08 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
index 502f463..00c5780 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
index 0ec3034..a967c02 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png
new file mode 100644
index 0000000..aece911
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png
new file mode 100644
index 0000000..4e3612c
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png
new file mode 100644
index 0000000..18f42d8
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png
new file mode 100644
index 0000000..6be07ee
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
index 0ec3034..a967c02 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
index e9f5fea..c10edda 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png
new file mode 100644
index 0000000..89488c2
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png
new file mode 100644
index 0000000..ccc5474
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
index 84ac32a..eaae3a6 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
index 8953cba..d2fc8a2 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
index 0467bf1..db97955 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
index 9da19ea..05a99ec 100644
Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
index 9da19ea..e1c87ab 100644
Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
index 9da19ea..84a1c25 100644
Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard
index f2e259c..14f7439 100644
--- a/ios/Runner/Base.lproj/LaunchScreen.storyboard
+++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -19,7 +19,7 @@
-
+
diff --git a/lib/core/routes/app_routes.dart b/lib/core/routes/app_routes.dart
index 6591757..7f7fbe3 100644
--- a/lib/core/routes/app_routes.dart
+++ b/lib/core/routes/app_routes.dart
@@ -9,17 +9,10 @@ class AppRoutes {
static const String futureValueSegment = 'future_value';
static const String depositsSegment = 'deposits';
static const String correctionSegment = 'correction';
- static const String correctionResultSegment = 'result';
- static const String historySegment = 'history';
- static const String profileSegment = 'profile';
// Absolutos para navegação
static const String financing = '/home/$financingSegment';
static const String futureValue = '$financing/$futureValueSegment';
static const String deposits = '$financing/$depositsSegment';
static const String correction = '/$correctionSegment/';
- static const String correctionResult =
- '/$correctionSegment/$correctionResultSegment';
- static const String history = '/home/$historySegment';
- static const String profile = '/home/$profileSegment';
}
diff --git a/lib/core/services/result_sharer.dart b/lib/core/services/result_sharer.dart
new file mode 100644
index 0000000..128fb6d
--- /dev/null
+++ b/lib/core/services/result_sharer.dart
@@ -0,0 +1,6 @@
+import 'dart:ui';
+
+abstract interface class ResultSharer {
+ /// [origin] ancora o popover no iPad.
+ Future share(String text, {Rect? origin});
+}
diff --git a/lib/core/services/share_plus_result_sharer.dart b/lib/core/services/share_plus_result_sharer.dart
new file mode 100644
index 0000000..ec573d4
--- /dev/null
+++ b/lib/core/services/share_plus_result_sharer.dart
@@ -0,0 +1,16 @@
+import 'dart:ui';
+
+import 'package:share_plus/share_plus.dart';
+
+import 'result_sharer.dart';
+
+class SharePlusResultSharer implements ResultSharer {
+ const SharePlusResultSharer();
+
+ @override
+ Future share(String text, {Rect? origin}) async {
+ await SharePlus.instance.share(
+ ShareParams(text: text, sharePositionOrigin: origin),
+ );
+ }
+}
diff --git a/lib/core/themes/app_colors.dart b/lib/core/themes/app_colors.dart
new file mode 100644
index 0000000..423a093
--- /dev/null
+++ b/lib/core/themes/app_colors.dart
@@ -0,0 +1,125 @@
+import 'package:flutter/material.dart';
+
+class AppColors extends ThemeExtension {
+ final Color background;
+ final Color sheet;
+ final Color elevated;
+ final Color hairline;
+ final Color textPrimary;
+ final Color textSecondary;
+ final Color textHint;
+ final Color error;
+ final Color scrim;
+ final Color bandHome;
+ final Color onBandSoft;
+ final Color brandAmber;
+ final Color onCalc;
+
+ const AppColors({
+ required this.background,
+ required this.sheet,
+ required this.elevated,
+ required this.hairline,
+ required this.textPrimary,
+ required this.textSecondary,
+ required this.textHint,
+ required this.error,
+ required this.scrim,
+ required this.bandHome,
+ required this.onBandSoft,
+ required this.brandAmber,
+ required this.onCalc,
+ });
+
+ static const light = AppColors(
+ background: Color(0xFFFFFFFF),
+ sheet: Color(0xFFFFFFFF),
+ elevated: Color(0xFFF2F2F5),
+ hairline: Color(0xFFE8E8EE),
+ textPrimary: Color(0xFF14121F),
+ textSecondary: Color(0xFF57546A),
+ textHint: Color(0xFF6E6B82),
+ error: Color(0xFFC92A2A),
+ scrim: Color(0x7314121F),
+ bandHome: Color(0xFF14121F),
+ onBandSoft: Color(0xFFB4B1C4),
+ brandAmber: Color(0xFFD4A657),
+ onCalc: Color(0xFFFFFFFF),
+ );
+
+ static const dark = AppColors(
+ background: Color(0xFF100E19),
+ sheet: Color(0xFF1B1926),
+ elevated: Color(0xFF262333),
+ hairline: Color(0x17FFFFFF),
+ textPrimary: Color(0xFFF5F4FA),
+ textSecondary: Color(0xFFB4B1C4),
+ textHint: Color(0xFF918DA5),
+ error: Color(0xFFFF8A8A),
+ scrim: Color(0xA805040A),
+ bandHome: Color(0xFF221F2E),
+ onBandSoft: Color(0xFFB4B1C4),
+ brandAmber: Color(0xFFD4A657),
+ onCalc: Color(0xFFFFFFFF),
+ );
+
+ /// Cores do tema atual; sem a extensão registrada, escolhe pelo brilho.
+ static AppColors of(BuildContext context) {
+ final theme = Theme.of(context);
+ return theme.extension() ??
+ (theme.brightness == Brightness.dark ? dark : light);
+ }
+
+ @override
+ AppColors copyWith({
+ Color? background,
+ Color? sheet,
+ Color? elevated,
+ Color? hairline,
+ Color? textPrimary,
+ Color? textSecondary,
+ Color? textHint,
+ Color? error,
+ Color? scrim,
+ Color? bandHome,
+ Color? onBandSoft,
+ Color? brandAmber,
+ Color? onCalc,
+ }) {
+ return AppColors(
+ background: background ?? this.background,
+ sheet: sheet ?? this.sheet,
+ elevated: elevated ?? this.elevated,
+ hairline: hairline ?? this.hairline,
+ textPrimary: textPrimary ?? this.textPrimary,
+ textSecondary: textSecondary ?? this.textSecondary,
+ textHint: textHint ?? this.textHint,
+ error: error ?? this.error,
+ scrim: scrim ?? this.scrim,
+ bandHome: bandHome ?? this.bandHome,
+ onBandSoft: onBandSoft ?? this.onBandSoft,
+ brandAmber: brandAmber ?? this.brandAmber,
+ onCalc: onCalc ?? this.onCalc,
+ );
+ }
+
+ @override
+ AppColors lerp(ThemeExtension? other, double t) {
+ if (other is! AppColors) return this;
+ return AppColors(
+ background: Color.lerp(background, other.background, t)!,
+ sheet: Color.lerp(sheet, other.sheet, t)!,
+ elevated: Color.lerp(elevated, other.elevated, t)!,
+ hairline: Color.lerp(hairline, other.hairline, t)!,
+ textPrimary: Color.lerp(textPrimary, other.textPrimary, t)!,
+ textSecondary: Color.lerp(textSecondary, other.textSecondary, t)!,
+ textHint: Color.lerp(textHint, other.textHint, t)!,
+ error: Color.lerp(error, other.error, t)!,
+ scrim: Color.lerp(scrim, other.scrim, t)!,
+ bandHome: Color.lerp(bandHome, other.bandHome, t)!,
+ onBandSoft: Color.lerp(onBandSoft, other.onBandSoft, t)!,
+ brandAmber: Color.lerp(brandAmber, other.brandAmber, t)!,
+ onCalc: Color.lerp(onCalc, other.onCalc, t)!,
+ );
+ }
+}
diff --git a/lib/core/themes/app_image.dart b/lib/core/themes/app_image.dart
index c01459c..bc17432 100644
--- a/lib/core/themes/app_image.dart
+++ b/lib/core/themes/app_image.dart
@@ -1,5 +1,6 @@
class AppImage {
- static const String _branding = 'assets/branding/';
+ static const String _branding = 'assets/branding';
static const String appLogo = '$_branding/app_icon.svg';
-}
\ No newline at end of file
+ static const String appLogoSmall = '$_branding/app_icon_pequeno.svg';
+}
diff --git a/lib/core/themes/app_theme.dart b/lib/core/themes/app_theme.dart
index ad0376b..b36d0d9 100644
--- a/lib/core/themes/app_theme.dart
+++ b/lib/core/themes/app_theme.dart
@@ -1,297 +1,88 @@
import 'package:flutter/material.dart';
-import 'package:real_calc/core/themes/extensions/help_card_theme.dart';
-import 'package:real_calc/core/themes/extensions/highlight_card_theme.dart';
-import 'package:real_calc/core/themes/extensions/menu_card_theme.dart';
-import 'package:real_calc/core/themes/extensions/options_bottom_forms_theme.dart';
-import 'color_tokens.dart';
-import 'extensions/financing_forms_theme.dart';
-import 'extensions/home_page_theme.dart';
-import 'extensions/index_picker_theme.dart';
-import 'extensions/input_forms_result_card_theme.dart';
-import 'extensions/metric_card_theme.dart';
-import 'extensions/page_header_theme.dart';
-import 'extensions/title_widget_theme.dart';
-import 'extensions/value_correction_result_theme.dart';
-import 'extensions/warning_banner_theme.dart';
-import 'extensions/error_banner_theme.dart';
+import 'package:flutter/services.dart';
+
+import 'app_colors.dart';
import 'text_styles.dart';
class AppTheme {
static ThemeData get darkTheme {
+ const colors = AppColors.dark;
return ThemeData(
brightness: Brightness.dark,
useMaterial3: true,
+ fontFamily: AppTextStyles.manropeFamily,
// Cores base
- scaffoldBackgroundColor: ColorTokens.background,
+ scaffoldBackgroundColor: colors.background,
// ColorScheme do Material 3
- colorScheme: const ColorScheme.dark(
- surface: ColorTokens.surface,
- error: ColorTokens.error,
- onPrimary: Colors.white,
- onSecondary: Colors.white,
- onSurface: ColorTokens.textPrimary,
+ colorScheme: ColorScheme.dark(
+ primary: colors.textPrimary,
+ surface: colors.sheet,
+ error: colors.error,
+ onPrimary: colors.background,
+ onSecondary: colors.background,
+ onSurface: colors.textPrimary,
onError: Colors.black,
),
-
- // AppBar global
- appBarTheme: AppBarTheme(
- backgroundColor: ColorTokens.surface,
- elevation: 0,
- centerTitle: true,
- actionsIconTheme: const IconThemeData(color: ColorTokens.accentAmber),
- titleTextStyle: AppTextStyles.headlineMedium,
- iconTheme: IconThemeData(color: ColorTokens.iconColor),
- ),
-
- // TextTheme
- textTheme: TextTheme(
- headlineLarge: AppTextStyles.headlineLarge,
- headlineMedium: AppTextStyles.headlineMedium,
- bodyLarge: AppTextStyles.bodyLarge,
- bodyMedium: AppTextStyles.bodyMedium,
- labelLarge: AppTextStyles.labelLarge,
- ),
-
- // Inputs
- inputDecorationTheme: InputDecorationTheme(
- filled: true,
- fillColor: ColorTokens.background,
- hintStyle: TextStyle(color: ColorTokens.textHint),
- enabledBorder: OutlineInputBorder(
- borderRadius: BorderRadius.circular(5),
- borderSide: const BorderSide(
- color: ColorTokens.background,
- width: 1.0,
- ),
- ),
- focusedBorder: OutlineInputBorder(
- borderRadius: BorderRadius.circular(5),
- borderSide: const BorderSide(
- color: ColorTokens.background,
- width: 1.5,
- ),
- ),
- errorBorder: OutlineInputBorder(
- borderRadius: BorderRadius.circular(16),
- borderSide: const BorderSide(color: ColorTokens.error, width: 1.0),
- ),
- focusedErrorBorder: OutlineInputBorder(
- borderRadius: BorderRadius.circular(16),
- borderSide: const BorderSide(color: ColorTokens.error, width: 1.5),
- ),
- contentPadding: const EdgeInsets.symmetric(
- vertical: 18,
- horizontal: 16,
- ),
- ),
-
- // Botões elevados
- elevatedButtonTheme: ElevatedButtonThemeData(
- style: ElevatedButton.styleFrom(
- backgroundColor: ColorTokens.surface,
- foregroundColor: Colors.white,
- minimumSize: const Size(double.infinity, 48),
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
- textStyle: AppTextStyles.labelLarge,
- ),
- ),
-
- // BottomNavigationBar
- bottomNavigationBarTheme: const BottomNavigationBarThemeData(
- backgroundColor: ColorTokens.background,
- selectedItemColor: ColorTokens.surface,
- unselectedItemColor: ColorTokens.textSecondary,
- type: BottomNavigationBarType.fixed,
- elevation: 8,
+ textSelectionTheme: TextSelectionThemeData(
+ cursorColor: colors.textPrimary,
+ selectionColor: colors.textPrimary.withValues(alpha: 0.32),
+ selectionHandleColor: colors.textPrimary,
),
- // SnackBar
snackBarTheme: SnackBarThemeData(
- backgroundColor: ColorTokens.surface,
- contentTextStyle: const TextStyle(color: ColorTokens.textPrimary),
+ backgroundColor: colors.bandHome,
+ contentTextStyle: AppTextStyles.detailLabel.copyWith(
+ color: Colors.white,
+ ),
+ actionTextColor: colors.brandAmber,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
behavior: SnackBarBehavior.floating,
),
- extensions: [
- const OptionsBottomFormsTheme(
- calculateButtonBackground: ColorTokens.accentAmber,
- calculateButtonForeground: Colors.white,
- actionButtonBackground: ColorTokens.background,
- actionButtonForeground: ColorTokens.textSecondary,
- borderButtonColor: ColorTokens.successContainerBorder,
- ),
- TitleWidgetTheme(
- titleStyle: AppTextStyles.headlineMedium.copyWith(
- color: ColorTokens.textPrimary,
- fontWeight: FontWeight.bold,
- ),
- subtitleStyle: AppTextStyles.bodyMedium.copyWith(
- color: ColorTokens.textSecondary,
- fontWeight: FontWeight.w500,
- ),
- ),
- HelpCardTheme(
- messageStyle: AppTextStyles.helpCardMessage,
- backgroundColor: ColorTokens.surface,
- borderColor: ColorTokens.accentAmber,
- iconBackgroundColor: ColorTokens.surface,
- iconColor: Colors.white,
- ),
- // app_theme.dart (dentro de extensions:)
- InputFormsResultCardTheme(
- neutralBorderColor: ColorTokens.border,
- neutralLabelColor: ColorTokens.textSecondary,
- neutralIconColor: ColorTokens.textSecondary,
- highlightedColor: ColorTokens.accentAmber,
- calculatedColor: ColorTokens.success,
- errorColor: ColorTokens.error,
- badgeStyle: AppTextStyles.stateBadge,
- ),
- IndexPickerTheme(
- backgroundColor: ColorTokens.surface,
- surfaceColor: ColorTokens.background,
- dividerColor: ColorTokens.border,
- selectedColor: ColorTokens.accentAmber.withValues(alpha: 0.12),
- selectedBorderColor: ColorTokens.accentAmber,
- titleColor: ColorTokens.textPrimary,
- subtitleColor: ColorTokens.textSecondary,
- checkColor: ColorTokens.accentAmber,
- sectionTitleStyle: AppTextStyles.labelLarge.copyWith(
- color: ColorTokens.textSecondary,
- letterSpacing: 1.2,
- ),
- itemTitleStyle: AppTextStyles.bodyLarge.copyWith(
- color: ColorTokens.textPrimary,
- fontWeight: FontWeight.w600,
- ),
- itemSubtitleStyle: AppTextStyles.bodyMedium.copyWith(
- color: ColorTokens.textSecondary,
- ),
- ),
- HomePageTheme(
- scaffoldBackgroundColor: ColorTokens.background,
- cardColor: ColorTokens.background,
- iconContainerColor: ColorTokens.iconColor,
- appNameStyle: AppTextStyles.appName,
- sectionHeaderStyle: AppTextStyles.sectionHeader,
- historyItemStyle: AppTextStyles.historyItem,
- historyItemIconColor: Colors.white,
- historyItemArrowColor: Colors.white,
- bottomNavBackgroundColor: ColorTokens.surface,
- bottomNavSelectedColor: ColorTokens.accentAmber,
- bottomNavUnselectedColor: Colors.white,
- lineColor: ColorTokens.border,
- ),
- FinancingFormsTheme(
- iconColor: ColorTokens.iconColor,
- progressIndicatorColor: ColorTokens.accentAmber,
- errorBackgroundColor: ColorTokens.errorContainerBg,
- errorBorderColor: ColorTokens.errorContainerBorder,
- errorTextStyle: AppTextStyles.errorBannerText,
- ),
- WarningBannerTheme(
- backgroundColor: ColorTokens.errorContainerBg,
- borderColor: ColorTokens.errorContainerBorder,
- iconColor: ColorTokens.error,
- textStyle: AppTextStyles.errorBannerText,
- ),
- ErrorBannerTheme(
- backgroundColor: ColorTokens.errorContainerBg,
- borderColor: ColorTokens.errorContainerBorder,
- iconColor: ColorTokens.errorContainerBorder,
- retryTextColor: Colors.white,
- messageStyle: AppTextStyles.errorBannerText,
- ),
- HighlightCardTheme(
- gradientColors: [
- ColorTokens.surface,
- ColorTokens.accentAmber.withValues(alpha: 0.32),
- ],
- titleStyle: AppTextStyles.menuCardTitleFeatured,
- subtitleStyle: AppTextStyles.menuCardDescriptionFeatured,
- buttonBackgroundColor: Colors.white,
- buttonForegroundColor: Colors.white,
- buttonTextStyle: AppTextStyles.menuCardDescriptionFeatured,
- ),
-
- MenuCardTheme(
- titleStyle: AppTextStyles.menuCardTitle,
- descriptionStyle: AppTextStyles.menuCardDescription,
- titleStyleCompact: AppTextStyles.menuCardTitleCompact,
- descriptionStyleCompact: AppTextStyles.menuCardDescriptionCompact,
- titleStyleFeatured: AppTextStyles.menuCardTitleFeatured,
- descriptionStyleFeatured: AppTextStyles.menuCardDescriptionFeatured,
- featuredBorderColor: ColorTokens.accentAmber,
- disabledLabelColor: ColorTokens.textHint,
- disabledTextStyle: AppTextStyles.menuCardDescription,
- ),
-
- MetricCardTheme(
- backgroundColor: ColorTokens.surface,
- borderColor: ColorTokens.accentAmber.withValues(alpha: 0.25),
- labelStyle: AppTextStyles.metricLabel,
- valueStyle: AppTextStyles.metricValue,
- valueUnitStyle: AppTextStyles.metricLabel,
- captionStyle: AppTextStyles.metricCaption,
- variationTextStyle: AppTextStyles.stateBadge,
- variationPositiveColor: ColorTokens.success,
- variationNegativeColor: ColorTokens.error,
- variationNeutralColor: ColorTokens.textSecondary,
- skeletonColor: ColorTokens.surfaceVariant,
- ),
- PageHeaderTheme(
- backgroundColor: ColorTokens.background,
- iconColor: ColorTokens.textSecondary,
- titleStyle: AppTextStyles.headlineMedium.copyWith(
- color: ColorTokens.textPrimary,
- ),
- ),
- ValueCorrectionResultTheme(
- cardBackgroundColor: ColorTokens.background,
- cardBorderColor: ColorTokens.border,
- spotlightBackground: ColorTokens.successContainerBg,
- spotlightBorderColor: ColorTokens.successContainerBorder,
- variationPositiveColor: ColorTokens.success,
- shareButtonColor: ColorTokens.accentAmber,
- editButtonColor: ColorTokens.textPrimary,
- titleStyle: AppTextStyles.headlineMedium.copyWith(
- color: ColorTokens.textPrimary,
- ),
- subtitleStyle: AppTextStyles.bodyMedium.copyWith(
- color: ColorTokens.textSecondary,
- ),
- spotlightLabelStyle: AppTextStyles.labelLarge.copyWith(
- color: ColorTokens.textPrimary,
- ),
- spotlightValueStyle: AppTextStyles.headlineLarge.copyWith(
- color: ColorTokens.textPrimary,
- fontSize: 32,
- fontWeight: FontWeight.w700,
- ),
- spotlightVariationStyle: AppTextStyles.stateBadge.copyWith(
- color: ColorTokens.success,
- fontWeight: FontWeight.w600,
- ),
- detailLabelStyle: AppTextStyles.bodyMedium.copyWith(
- color: ColorTokens.textSecondary,
- ),
- detailValueStyle: AppTextStyles.bodyLarge.copyWith(
- color: ColorTokens.textPrimary,
- fontWeight: FontWeight.w500,
- ),
- linkStyle: AppTextStyles.bodyMedium.copyWith(
- color: ColorTokens.textPrimary,
- decoration: TextDecoration.underline,
- ),
- ),
- ],
+ extensions: const [colors],
);
}
static ThemeData get lightTheme {
- return darkTheme;
+ final base = darkTheme;
+ const colors = AppColors.light;
+ return base.copyWith(
+ brightness: Brightness.light,
+ scaffoldBackgroundColor: colors.background,
+ colorScheme: ColorScheme.light(
+ primary: colors.textPrimary,
+ surface: colors.sheet,
+ error: colors.error,
+ onPrimary: colors.onCalc,
+ onSecondary: colors.onCalc,
+ onSurface: colors.textPrimary,
+ onError: colors.onCalc,
+ ),
+ textSelectionTheme: TextSelectionThemeData(
+ cursorColor: colors.textPrimary,
+ selectionColor: colors.textPrimary.withValues(alpha: 0.32),
+ selectionHandleColor: colors.textPrimary,
+ ),
+ snackBarTheme: base.snackBarTheme.copyWith(
+ backgroundColor: colors.bandHome,
+ ),
+ extensions: const [colors],
+ );
+ }
+
+ static SystemUiOverlayStyle systemUiOverlayStyle(AppColors colors) {
+ return SystemUiOverlayStyle(
+ statusBarColor: Colors.transparent,
+ statusBarIconBrightness: Brightness.light,
+ statusBarBrightness: Brightness.dark,
+ systemNavigationBarColor: colors.background,
+ systemNavigationBarIconBrightness: colors == AppColors.light
+ ? Brightness.dark
+ : Brightness.light,
+ systemNavigationBarDividerColor: colors.background,
+ systemNavigationBarContrastEnforced: false,
+ );
}
}
diff --git a/lib/core/themes/calculator_palette.dart b/lib/core/themes/calculator_palette.dart
new file mode 100644
index 0000000..5012a41
--- /dev/null
+++ b/lib/core/themes/calculator_palette.dart
@@ -0,0 +1,54 @@
+import 'package:flutter/material.dart';
+
+enum CalculatorKind { financing, deposits, futureValue, correction }
+
+class CalculatorPalette {
+ final Color solid;
+ final Color onSolid;
+ final Color text;
+
+ const CalculatorPalette({
+ required this.solid,
+ required this.onSolid,
+ required this.text,
+ });
+
+ static CalculatorPalette of(
+ CalculatorKind kind, {
+ Brightness brightness = Brightness.light,
+ }) {
+ final isDark = brightness == Brightness.dark;
+ return switch (kind) {
+ CalculatorKind.financing => CalculatorPalette(
+ solid: const Color(0xFF364FC7),
+ onSolid: Colors.white,
+ text: Color(isDark ? 0xFF91A7FF : 0xFF364FC7),
+ ),
+ CalculatorKind.deposits => CalculatorPalette(
+ solid: const Color(0xFF087F5B),
+ onSolid: Colors.white,
+ text: Color(isDark ? 0xFF63E6BE : 0xFF0A6E50),
+ ),
+ CalculatorKind.futureValue => CalculatorPalette(
+ solid: const Color(0xFF6741D9),
+ onSolid: Colors.white,
+ text: Color(isDark ? 0xFFB197FC : 0xFF6741D9),
+ ),
+ CalculatorKind.correction => CalculatorPalette(
+ solid: const Color(0xFFB85300),
+ onSolid: Colors.white,
+ text: Color(isDark ? 0xFFFFA94D : 0xFFA04600),
+ ),
+ };
+ }
+
+ @override
+ bool operator ==(Object other) =>
+ other is CalculatorPalette &&
+ other.solid == solid &&
+ other.onSolid == onSolid &&
+ other.text == text;
+
+ @override
+ int get hashCode => Object.hash(solid, onSolid, text);
+}
diff --git a/lib/core/themes/calculator_scope.dart b/lib/core/themes/calculator_scope.dart
new file mode 100644
index 0000000..946184e
--- /dev/null
+++ b/lib/core/themes/calculator_scope.dart
@@ -0,0 +1,55 @@
+import 'package:flutter/material.dart';
+
+import 'calculator_palette.dart';
+
+class CalculatorScope extends StatelessWidget {
+ final CalculatorKind kind;
+ final Widget child;
+
+ const CalculatorScope({required this.kind, required this.child, super.key});
+
+ static CalculatorPalette? maybeOf(BuildContext context) => context
+ .dependOnInheritedWidgetOfExactType<_CalculatorPaletteScope>()
+ ?.palette;
+
+ static CalculatorPalette of(BuildContext context) {
+ final palette = maybeOf(context);
+ assert(
+ palette != null,
+ 'CalculatorScope não encontrado na árvore de widgets.',
+ );
+ return palette!;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final palette = CalculatorPalette.of(
+ kind,
+ brightness: Theme.of(context).brightness,
+ );
+ final theme = Theme.of(context);
+ final localTheme = theme.copyWith(
+ colorScheme: theme.colorScheme.copyWith(primary: palette.solid),
+ textSelectionTheme: TextSelectionThemeData(
+ cursorColor: palette.solid,
+ selectionColor: palette.solid.withValues(alpha: 0.32),
+ selectionHandleColor: palette.solid,
+ ),
+ );
+
+ return Theme(
+ data: localTheme,
+ child: _CalculatorPaletteScope(palette: palette, child: child),
+ );
+ }
+}
+
+class _CalculatorPaletteScope extends InheritedWidget {
+ final CalculatorPalette palette;
+
+ const _CalculatorPaletteScope({required this.palette, required super.child});
+
+ @override
+ bool updateShouldNotify(_CalculatorPaletteScope oldWidget) =>
+ palette != oldWidget.palette;
+}
diff --git a/lib/core/themes/color_tokens.dart b/lib/core/themes/color_tokens.dart
deleted file mode 100644
index 099e548..0000000
--- a/lib/core/themes/color_tokens.dart
+++ /dev/null
@@ -1,34 +0,0 @@
-import 'package:flutter/material.dart';
-
-abstract class ColorTokens {
- // Superfícies
- static const Color background = Color(0xFF0A0D12);
- static const Color surface = Color(0xFF0E1116);
- static const Color surfaceVariant = Color(0xFF12161D);
-
- // Texto
- static const Color textPrimary = Color(0xFFEDEFF2);
- static const Color textSecondary = Color(0xFF8B929C);
- static const Color textHint = Color(0xFF5C636E);
-
- // Borda
- static const Color border = Color.fromARGB(20, 255, 255, 255);
-
- static const Color accentAmber = Color(0xFFD4A657);
-
- // Feedback
- static const Color error = Color(0xFFE24B4A);
- static const Color success = Color(0xFF6FCF97);
- static const Color warning = Color(0xFFFFC107);
-
- // Container de erro
- static const Color errorContainerBg = Color.fromARGB(25, 226, 75, 74);
- static const Color errorContainerBorder = Color.fromARGB(25, 226, 75, 74);
-
- // Container de success
- static const Color successContainerBg = Color.fromARGB(25, 111, 207, 151);
- static const Color successContainerBorder = Color.fromARGB(25, 111, 207, 151);
-
- //icons
- static const Color iconColor = Color(0xFFFFFFFF);
-}
\ No newline at end of file
diff --git a/lib/core/themes/extensions/error_banner_theme.dart b/lib/core/themes/extensions/error_banner_theme.dart
deleted file mode 100644
index 1c180be..0000000
--- a/lib/core/themes/extensions/error_banner_theme.dart
+++ /dev/null
@@ -1,77 +0,0 @@
-import 'package:flutter/material.dart';
-
-class ErrorBannerTheme extends ThemeExtension {
- final Color backgroundColor;
- final Color borderColor;
- final Color iconColor;
- final Color retryTextColor;
- final TextStyle messageStyle;
- final EdgeInsetsGeometry padding;
- final double borderRadius;
- final double iconSize;
- final double iconMessageSpacing;
- final double retrySpacing;
-
- const ErrorBannerTheme({
- required this.backgroundColor,
- required this.borderColor,
- required this.iconColor,
- required this.retryTextColor,
- required this.messageStyle,
- this.padding = const EdgeInsets.all(12),
- this.borderRadius = 12,
- this.iconSize = 18,
- this.iconMessageSpacing = 10,
- this.retrySpacing = 8,
- });
-
- @override
- ErrorBannerTheme copyWith({
- Color? backgroundColor,
- Color? borderColor,
- Color? iconColor,
- Color? retryTextColor,
- TextStyle? messageStyle,
- EdgeInsetsGeometry? padding,
- double? borderRadius,
- double? iconSize,
- double? iconMessageSpacing,
- double? retrySpacing,
- }) {
- return ErrorBannerTheme(
- backgroundColor: backgroundColor ?? this.backgroundColor,
- borderColor: borderColor ?? this.borderColor,
- iconColor: iconColor ?? this.iconColor,
- retryTextColor: retryTextColor ?? this.retryTextColor,
- messageStyle: messageStyle ?? this.messageStyle,
- padding: padding ?? this.padding,
- borderRadius: borderRadius ?? this.borderRadius,
- iconSize: iconSize ?? this.iconSize,
- iconMessageSpacing: iconMessageSpacing ?? this.iconMessageSpacing,
- retrySpacing: retrySpacing ?? this.retrySpacing,
- );
- }
-
- @override
- ErrorBannerTheme lerp(
- covariant ThemeExtension? other,
- double t,
- ) {
- if (other is! ErrorBannerTheme) return this;
-
- return ErrorBannerTheme(
- backgroundColor: Color.lerp(backgroundColor, other.backgroundColor, t)!,
- borderColor: Color.lerp(borderColor, other.borderColor, t)!,
- iconColor: Color.lerp(iconColor, other.iconColor, t)!,
- retryTextColor: Color.lerp(retryTextColor, other.retryTextColor, t)!,
- messageStyle: TextStyle.lerp(messageStyle, other.messageStyle, t)!,
- padding: EdgeInsetsGeometry.lerp(padding, other.padding, t)!,
- borderRadius: borderRadius + (other.borderRadius - borderRadius) * t,
- iconSize: iconSize + (other.iconSize - iconSize) * t,
- iconMessageSpacing:
- iconMessageSpacing +
- (other.iconMessageSpacing - iconMessageSpacing) * t,
- retrySpacing: retrySpacing + (other.retrySpacing - retrySpacing) * t,
- );
- }
-}
diff --git a/lib/core/themes/extensions/financing_forms_theme.dart b/lib/core/themes/extensions/financing_forms_theme.dart
deleted file mode 100644
index 1cfa1b0..0000000
--- a/lib/core/themes/extensions/financing_forms_theme.dart
+++ /dev/null
@@ -1,61 +0,0 @@
-import 'package:flutter/material.dart';
-
-class FinancingFormsTheme extends ThemeExtension {
- final Color iconColor;
- final Color progressIndicatorColor;
- final Color errorBackgroundColor;
- final Color errorBorderColor;
- final TextStyle errorTextStyle;
- final EdgeInsetsGeometry errorContainerPadding;
-
- const FinancingFormsTheme({
- required this.iconColor,
- required this.progressIndicatorColor,
- required this.errorBackgroundColor,
- required this.errorBorderColor,
- required this.errorTextStyle,
- this.errorContainerPadding = const EdgeInsets.all(12),
- });
-
- @override
- ThemeExtension copyWith({
- Color? iconColor,
- Color? progressIndicatorColor,
- Color? errorBackgroundColor,
- Color? errorBorderColor,
- TextStyle? errorTextStyle,
- EdgeInsetsGeometry? errorContainerPadding,
- }) {
- return FinancingFormsTheme(
- iconColor: iconColor ?? this.iconColor,
- progressIndicatorColor:
- progressIndicatorColor ?? this.progressIndicatorColor,
- errorBackgroundColor:
- errorBackgroundColor ?? this.errorBackgroundColor,
- errorBorderColor: errorBorderColor ?? this.errorBorderColor,
- errorTextStyle: errorTextStyle ?? this.errorTextStyle,
- errorContainerPadding:
- errorContainerPadding ?? this.errorContainerPadding,
- );
- }
-
- @override
- ThemeExtension lerp(
- ThemeExtension? other,
- double t,
- ) {
- if (other is! FinancingFormsTheme) return this;
- return FinancingFormsTheme(
- iconColor: Color.lerp(iconColor, other.iconColor, t)!,
- progressIndicatorColor:
- Color.lerp(progressIndicatorColor, other.progressIndicatorColor, t)!,
- errorBackgroundColor:
- Color.lerp(errorBackgroundColor, other.errorBackgroundColor, t)!,
- errorBorderColor:
- Color.lerp(errorBorderColor, other.errorBorderColor, t)!,
- errorTextStyle:
- TextStyle.lerp(errorTextStyle, other.errorTextStyle, t)!,
- errorContainerPadding: other.errorContainerPadding,
- );
- }
-}
\ No newline at end of file
diff --git a/lib/core/themes/extensions/help_card_theme.dart b/lib/core/themes/extensions/help_card_theme.dart
deleted file mode 100644
index b50ceda..0000000
--- a/lib/core/themes/extensions/help_card_theme.dart
+++ /dev/null
@@ -1,65 +0,0 @@
-import 'package:flutter/material.dart';
-
-class HelpCardTheme extends ThemeExtension {
- final TextStyle messageStyle;
- final Color backgroundColor;
- final Color borderColor;
- final Color iconBackgroundColor;
- final Color iconColor;
- final double iconSize;
- final double borderRadius;
- final EdgeInsetsGeometry padding;
-
- const HelpCardTheme({
- required this.messageStyle,
- required this.backgroundColor,
- required this.borderColor,
- required this.iconBackgroundColor,
- required this.iconColor,
- this.iconSize = 22,
- this.borderRadius = 18,
- this.padding = const EdgeInsets.all(16),
- });
-
- @override
- ThemeExtension copyWith({
- TextStyle? messageStyle,
- Color? backgroundColor,
- Color? borderColor,
- Color? iconBackgroundColor,
- Color? iconColor,
- double? iconSize,
- double? borderRadius,
- EdgeInsetsGeometry? padding,
- }) {
- return HelpCardTheme(
- messageStyle: messageStyle ?? this.messageStyle,
- backgroundColor: backgroundColor ?? this.backgroundColor,
- borderColor: borderColor ?? this.borderColor,
- iconBackgroundColor: iconBackgroundColor ?? this.iconBackgroundColor,
- iconColor: iconColor ?? this.iconColor,
- iconSize: iconSize ?? this.iconSize,
- borderRadius: borderRadius ?? this.borderRadius,
- padding: padding ?? this.padding,
- );
- }
-
- @override
- ThemeExtension lerp(
- ThemeExtension? other,
- double t,
- ) {
- if (other is! HelpCardTheme) return this;
- return HelpCardTheme(
- messageStyle: TextStyle.lerp(messageStyle, other.messageStyle, t)!,
- backgroundColor: Color.lerp(backgroundColor, other.backgroundColor, t)!,
- borderColor: Color.lerp(borderColor, other.borderColor, t)!,
- iconBackgroundColor:
- Color.lerp(iconBackgroundColor, other.iconBackgroundColor, t)!,
- iconColor: Color.lerp(iconColor, other.iconColor, t)!,
- iconSize: other.iconSize,
- borderRadius: other.borderRadius,
- padding: other.padding,
- );
- }
-}
\ No newline at end of file
diff --git a/lib/core/themes/extensions/highlight_card_theme.dart b/lib/core/themes/extensions/highlight_card_theme.dart
deleted file mode 100644
index 0ba29d7..0000000
--- a/lib/core/themes/extensions/highlight_card_theme.dart
+++ /dev/null
@@ -1,79 +0,0 @@
-import 'package:flutter/material.dart';
-
-class HighlightCardTheme extends ThemeExtension {
- final List gradientColors;
- final AlignmentGeometry gradientBegin;
- final AlignmentGeometry gradientEnd;
- final TextStyle titleStyle;
- final TextStyle subtitleStyle;
- final Color buttonBackgroundColor;
- final Color buttonForegroundColor;
- final TextStyle buttonTextStyle;
- final double borderRadius;
- final EdgeInsetsGeometry padding;
-
- const HighlightCardTheme({
- required this.gradientColors,
- this.gradientBegin = Alignment.topLeft,
- this.gradientEnd = Alignment.bottomRight,
- required this.titleStyle,
- required this.subtitleStyle,
- required this.buttonBackgroundColor,
- required this.buttonForegroundColor,
- required this.buttonTextStyle,
- this.borderRadius = 20,
- this.padding = const EdgeInsets.all(24),
- });
-
- @override
- ThemeExtension copyWith({
- List? gradientColors,
- AlignmentGeometry? gradientBegin,
- AlignmentGeometry? gradientEnd,
- TextStyle? titleStyle,
- TextStyle? subtitleStyle,
- Color? buttonBackgroundColor,
- Color? buttonForegroundColor,
- TextStyle? buttonTextStyle,
- double? borderRadius,
- EdgeInsetsGeometry? padding,
- }) {
- return HighlightCardTheme(
- gradientColors: gradientColors ?? this.gradientColors,
- gradientBegin: gradientBegin ?? this.gradientBegin,
- gradientEnd: gradientEnd ?? this.gradientEnd,
- titleStyle: titleStyle ?? this.titleStyle,
- subtitleStyle: subtitleStyle ?? this.subtitleStyle,
- buttonBackgroundColor:
- buttonBackgroundColor ?? this.buttonBackgroundColor,
- buttonForegroundColor:
- buttonForegroundColor ?? this.buttonForegroundColor,
- buttonTextStyle: buttonTextStyle ?? this.buttonTextStyle,
- borderRadius: borderRadius ?? this.borderRadius,
- padding: padding ?? this.padding,
- );
- }
-
- @override
- ThemeExtension lerp(
- ThemeExtension? other,
- double t,
- ) {
- if (other is! HighlightCardTheme) return this;
- return HighlightCardTheme(
- gradientColors: other.gradientColors,
- gradientBegin: other.gradientBegin,
- gradientEnd: other.gradientEnd,
- titleStyle: TextStyle.lerp(titleStyle, other.titleStyle, t)!,
- subtitleStyle: TextStyle.lerp(subtitleStyle, other.subtitleStyle, t)!,
- buttonBackgroundColor:
- Color.lerp(buttonBackgroundColor, other.buttonBackgroundColor, t)!,
- buttonForegroundColor:
- Color.lerp(buttonForegroundColor, other.buttonForegroundColor, t)!,
- buttonTextStyle:
- TextStyle.lerp(buttonTextStyle, other.buttonTextStyle, t)!,
- borderRadius: other.borderRadius,
- padding: other.padding,
- );
- }
-}
\ No newline at end of file
diff --git a/lib/core/themes/extensions/home_page_theme.dart b/lib/core/themes/extensions/home_page_theme.dart
deleted file mode 100644
index b269e3e..0000000
--- a/lib/core/themes/extensions/home_page_theme.dart
+++ /dev/null
@@ -1,82 +0,0 @@
-import 'package:flutter/material.dart';
-
-class HomePageTheme extends ThemeExtension {
- final Color scaffoldBackgroundColor;
- final Color cardColor;
- final Color iconContainerColor;
- final TextStyle appNameStyle;
- final TextStyle sectionHeaderStyle;
- final TextStyle historyItemStyle;
- final Color historyItemIconColor;
- final Color historyItemArrowColor;
- final Color bottomNavBackgroundColor;
- final Color bottomNavSelectedColor;
- final Color bottomNavUnselectedColor;
- final Color lineColor;
-
- const HomePageTheme({
- required this.scaffoldBackgroundColor,
- required this.cardColor,
- required this.iconContainerColor,
- required this.appNameStyle,
- required this.sectionHeaderStyle,
- required this.historyItemStyle,
- required this.historyItemIconColor,
- required this.historyItemArrowColor,
- required this.bottomNavBackgroundColor,
- required this.bottomNavSelectedColor,
- required this.bottomNavUnselectedColor,
- required this.lineColor,
- });
-
- @override
- ThemeExtension copyWith({
- Color? scaffoldBackgroundColor,
- Color? cardColor,
- Color? primaryBlue,
- TextStyle? appNameStyle,
- TextStyle? sectionHeaderStyle,
- TextStyle? historyItemStyle,
- Color? historyItemIconColor,
- Color? historyItemArrowColor,
- Color? bottomNavBackgroundColor,
- Color? bottomNavSelectedColor,
- Color? bottomNavUnselectedColor,
- Color? iconContainerColor,
- Color? lineColor,
- }) {
- return HomePageTheme(
- scaffoldBackgroundColor: scaffoldBackgroundColor ?? this.scaffoldBackgroundColor,
- cardColor: cardColor ?? this.cardColor,
- iconContainerColor: iconContainerColor ?? this.iconContainerColor,
- appNameStyle: appNameStyle ?? this.appNameStyle,
- sectionHeaderStyle: sectionHeaderStyle ?? this.sectionHeaderStyle,
- historyItemStyle: historyItemStyle ?? this.historyItemStyle,
- historyItemIconColor: historyItemIconColor ?? this.historyItemIconColor,
- historyItemArrowColor: historyItemArrowColor ?? this.historyItemArrowColor,
- bottomNavBackgroundColor: bottomNavBackgroundColor ?? this.bottomNavBackgroundColor,
- bottomNavSelectedColor: bottomNavSelectedColor ?? this.bottomNavSelectedColor,
- bottomNavUnselectedColor: bottomNavUnselectedColor ?? this.bottomNavUnselectedColor,
- lineColor: lineColor ?? this.lineColor,
- );
- }
-
- @override
- ThemeExtension lerp(ThemeExtension? other, double t) {
- if (other is! HomePageTheme) return this;
- return HomePageTheme(
- scaffoldBackgroundColor: Color.lerp(scaffoldBackgroundColor, other.scaffoldBackgroundColor, t)!,
- cardColor: Color.lerp(cardColor, other.cardColor, t)!,
- iconContainerColor: Color.lerp(iconContainerColor, other.iconContainerColor, t)!,
- appNameStyle: TextStyle.lerp(appNameStyle, other.appNameStyle, t)!,
- sectionHeaderStyle: TextStyle.lerp(sectionHeaderStyle, other.sectionHeaderStyle, t)!,
- historyItemStyle: TextStyle.lerp(historyItemStyle, other.historyItemStyle, t)!,
- historyItemIconColor: Color.lerp(historyItemIconColor, other.historyItemIconColor, t)!,
- historyItemArrowColor: Color.lerp(historyItemArrowColor, other.historyItemArrowColor, t)!,
- bottomNavBackgroundColor: Color.lerp(bottomNavBackgroundColor, other.bottomNavBackgroundColor, t)!,
- bottomNavSelectedColor: Color.lerp(bottomNavSelectedColor, other.bottomNavSelectedColor, t)!,
- bottomNavUnselectedColor: Color.lerp(bottomNavUnselectedColor, other.bottomNavUnselectedColor, t)!,
- lineColor: Color.lerp(lineColor, other.lineColor, t)!,
- );
- }
-}
\ No newline at end of file
diff --git a/lib/core/themes/extensions/index_picker_theme.dart b/lib/core/themes/extensions/index_picker_theme.dart
deleted file mode 100644
index d30f006..0000000
--- a/lib/core/themes/extensions/index_picker_theme.dart
+++ /dev/null
@@ -1,80 +0,0 @@
-import 'package:flutter/material.dart';
-
-class IndexPickerTheme extends ThemeExtension {
- final Color backgroundColor;
- final Color surfaceColor;
- final Color dividerColor;
- final Color selectedColor;
- final Color selectedBorderColor;
- final Color titleColor;
- final Color subtitleColor;
- final Color checkColor;
- final TextStyle sectionTitleStyle;
- final TextStyle itemTitleStyle;
- final TextStyle itemSubtitleStyle;
-
- const IndexPickerTheme({
- required this.backgroundColor,
- required this.surfaceColor,
- required this.dividerColor,
- required this.selectedColor,
- required this.selectedBorderColor,
- required this.titleColor,
- required this.subtitleColor,
- required this.checkColor,
- required this.sectionTitleStyle,
- required this.itemTitleStyle,
- required this.itemSubtitleStyle,
- });
-
- @override
- IndexPickerTheme copyWith({
- Color? backgroundColor,
- Color? surfaceColor,
- Color? dividerColor,
- Color? selectedColor,
- Color? selectedBorderColor,
- Color? titleColor,
- Color? subtitleColor,
- Color? checkColor,
- TextStyle? sectionTitleStyle,
- TextStyle? itemTitleStyle,
- TextStyle? itemSubtitleStyle,
- }) {
- return IndexPickerTheme(
- backgroundColor: backgroundColor ?? this.backgroundColor,
- surfaceColor: surfaceColor ?? this.surfaceColor,
- dividerColor: dividerColor ?? this.dividerColor,
- selectedColor: selectedColor ?? this.selectedColor,
- selectedBorderColor: selectedBorderColor ?? this.selectedBorderColor,
- titleColor: titleColor ?? this.titleColor,
- subtitleColor: subtitleColor ?? this.subtitleColor,
- checkColor: checkColor ?? this.checkColor,
- sectionTitleStyle: sectionTitleStyle ?? this.sectionTitleStyle,
- itemTitleStyle: itemTitleStyle ?? this.itemTitleStyle,
- itemSubtitleStyle: itemSubtitleStyle ?? this.itemSubtitleStyle,
- );
- }
-
- @override
- IndexPickerTheme lerp(ThemeExtension? other, double t) {
- if (other is! IndexPickerTheme) return this;
-
- return IndexPickerTheme(
- backgroundColor: Color.lerp(backgroundColor, other.backgroundColor, t)!,
- surfaceColor: Color.lerp(surfaceColor, other.surfaceColor, t)!,
- dividerColor: Color.lerp(dividerColor, other.dividerColor, t)!,
- selectedColor: Color.lerp(selectedColor, other.selectedColor, t)!,
- selectedBorderColor:
- Color.lerp(selectedBorderColor, other.selectedBorderColor, t)!,
- titleColor: Color.lerp(titleColor, other.titleColor, t)!,
- subtitleColor: Color.lerp(subtitleColor, other.subtitleColor, t)!,
- checkColor: Color.lerp(checkColor, other.checkColor, t)!,
- sectionTitleStyle:
- TextStyle.lerp(sectionTitleStyle, other.sectionTitleStyle, t)!,
- itemTitleStyle: TextStyle.lerp(itemTitleStyle, other.itemTitleStyle, t)!,
- itemSubtitleStyle:
- TextStyle.lerp(itemSubtitleStyle, other.itemSubtitleStyle, t)!,
- );
- }
-}
diff --git a/lib/core/themes/extensions/input_forms_result_card_theme.dart b/lib/core/themes/extensions/input_forms_result_card_theme.dart
deleted file mode 100644
index 870c91a..0000000
--- a/lib/core/themes/extensions/input_forms_result_card_theme.dart
+++ /dev/null
@@ -1,67 +0,0 @@
-import 'package:flutter/material.dart';
-
-class InputFormsResultCardTheme extends ThemeExtension {
- final Color neutralBorderColor;
- final Color neutralLabelColor;
- final Color neutralIconColor;
- final Color highlightedColor;
- final Color calculatedColor;
- final Color errorColor;
- final TextStyle badgeStyle;
- final double borderRadius;
-
- const InputFormsResultCardTheme({
- required this.neutralBorderColor,
- required this.neutralLabelColor,
- required this.neutralIconColor,
- required this.highlightedColor,
- required this.calculatedColor,
- required this.errorColor,
- required this.badgeStyle,
- this.borderRadius = 14,
- });
-
- @override
- InputFormsResultCardTheme copyWith({
- Color? neutralBorderColor,
- Color? neutralLabelColor,
- Color? neutralIconColor,
- Color? highlightedColor,
- Color? calculatedColor,
- Color? errorColor,
- TextStyle? badgeStyle,
- double? borderRadius,
- }) {
- return InputFormsResultCardTheme(
- neutralBorderColor: neutralBorderColor ?? this.neutralBorderColor,
- neutralLabelColor: neutralLabelColor ?? this.neutralLabelColor,
- neutralIconColor: neutralIconColor ?? this.neutralIconColor,
- highlightedColor: highlightedColor ?? this.highlightedColor,
- calculatedColor: calculatedColor ?? this.calculatedColor,
- errorColor: errorColor ?? this.errorColor,
- badgeStyle: badgeStyle ?? this.badgeStyle,
- borderRadius: borderRadius ?? this.borderRadius,
- );
- }
-
- @override
- InputFormsResultCardTheme lerp(
- ThemeExtension? other,
- double t,
- ) {
- if (other is! InputFormsResultCardTheme) return this;
- return InputFormsResultCardTheme(
- neutralBorderColor:
- Color.lerp(neutralBorderColor, other.neutralBorderColor, t)!,
- neutralLabelColor:
- Color.lerp(neutralLabelColor, other.neutralLabelColor, t)!,
- neutralIconColor:
- Color.lerp(neutralIconColor, other.neutralIconColor, t)!,
- highlightedColor: Color.lerp(highlightedColor, other.highlightedColor, t)!,
- calculatedColor: Color.lerp(calculatedColor, other.calculatedColor, t)!,
- errorColor: Color.lerp(errorColor, other.errorColor, t)!,
- badgeStyle: TextStyle.lerp(badgeStyle, other.badgeStyle, t)!,
- borderRadius: borderRadius + (other.borderRadius - borderRadius) * t,
- );
- }
-}
\ No newline at end of file
diff --git a/lib/core/themes/extensions/menu_card_theme.dart b/lib/core/themes/extensions/menu_card_theme.dart
deleted file mode 100644
index 3d4215f..0000000
--- a/lib/core/themes/extensions/menu_card_theme.dart
+++ /dev/null
@@ -1,140 +0,0 @@
-import 'dart:ui';
-import 'package:flutter/material.dart';
-
-class MenuCardTheme extends ThemeExtension {
- // Variante standard (grid 2 colunas)
- final TextStyle titleStyle;
- final TextStyle descriptionStyle;
- final TextStyle titleStyleCompact;
- final TextStyle descriptionStyleCompact;
- final double iconSize;
- final double iconSizeCompact;
- final double borderRadius;
- final EdgeInsetsGeometry padding;
- final EdgeInsetsGeometry compactPadding;
- final double heightThreshold;
-
- // Variante featured (tile full-width, texto maior por ter mais espaço)
- final TextStyle titleStyleFeatured;
- final TextStyle descriptionStyleFeatured;
- final Color featuredBorderColor;
-
- // Variante disabled
- final Color disabledLabelColor;
- final TextStyle disabledTextStyle;
-
- const MenuCardTheme({
- required this.titleStyle,
- required this.descriptionStyle,
- required this.titleStyleCompact,
- required this.descriptionStyleCompact,
- required this.titleStyleFeatured,
- required this.descriptionStyleFeatured,
- required this.featuredBorderColor,
- required this.disabledLabelColor,
- required this.disabledTextStyle,
- this.iconSize = 24,
- this.iconSizeCompact = 20,
- this.borderRadius = 20,
- this.padding = const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
- this.compactPadding = const EdgeInsets.all(8),
- this.heightThreshold = 100,
- });
-
- @override
- MenuCardTheme copyWith({
- TextStyle? titleStyle,
- TextStyle? descriptionStyle,
- TextStyle? titleStyleCompact,
- TextStyle? descriptionStyleCompact,
- TextStyle? titleStyleFeatured,
- TextStyle? descriptionStyleFeatured,
- Color? featuredBorderColor,
- Color? disabledLabelColor,
- TextStyle? disabledTextStyle,
- double? iconSize,
- double? iconSizeCompact,
- double? borderRadius,
- EdgeInsetsGeometry? padding,
- EdgeInsetsGeometry? compactPadding,
- double? heightThreshold,
- }) {
- return MenuCardTheme(
- titleStyle: titleStyle ?? this.titleStyle,
- descriptionStyle: descriptionStyle ?? this.descriptionStyle,
- titleStyleCompact: titleStyleCompact ?? this.titleStyleCompact,
- descriptionStyleCompact:
- descriptionStyleCompact ?? this.descriptionStyleCompact,
- titleStyleFeatured: titleStyleFeatured ?? this.titleStyleFeatured,
- descriptionStyleFeatured:
- descriptionStyleFeatured ?? this.descriptionStyleFeatured,
- featuredBorderColor: featuredBorderColor ?? this.featuredBorderColor,
- disabledLabelColor: disabledLabelColor ?? this.disabledLabelColor,
- disabledTextStyle: disabledTextStyle ?? this.disabledTextStyle,
- iconSize: iconSize ?? this.iconSize,
- iconSizeCompact: iconSizeCompact ?? this.iconSizeCompact,
- borderRadius: borderRadius ?? this.borderRadius,
- padding: padding ?? this.padding,
- compactPadding: compactPadding ?? this.compactPadding,
- heightThreshold: heightThreshold ?? this.heightThreshold,
- );
- }
-
- @override
- MenuCardTheme lerp(ThemeExtension? other, double t) {
- if (other is! MenuCardTheme) return this;
- return MenuCardTheme(
- titleStyle: TextStyle.lerp(titleStyle, other.titleStyle, t)!,
- descriptionStyle: TextStyle.lerp(
- descriptionStyle,
- other.descriptionStyle,
- t,
- )!,
- titleStyleCompact: TextStyle.lerp(
- titleStyleCompact,
- other.titleStyleCompact,
- t,
- )!,
- descriptionStyleCompact: TextStyle.lerp(
- descriptionStyleCompact,
- other.descriptionStyleCompact,
- t,
- )!,
- titleStyleFeatured: TextStyle.lerp(
- titleStyleFeatured,
- other.titleStyleFeatured,
- t,
- )!,
- descriptionStyleFeatured: TextStyle.lerp(
- descriptionStyleFeatured,
- other.descriptionStyleFeatured,
- t,
- )!,
- featuredBorderColor: Color.lerp(
- featuredBorderColor,
- other.featuredBorderColor,
- t,
- )!,
- disabledLabelColor: Color.lerp(
- disabledLabelColor,
- other.disabledLabelColor,
- t,
- )!,
- disabledTextStyle: TextStyle.lerp(
- disabledTextStyle,
- other.disabledTextStyle,
- t,
- )!,
- iconSize: lerpDouble(iconSize, other.iconSize, t)!,
- iconSizeCompact: lerpDouble(iconSizeCompact, other.iconSizeCompact, t)!,
- borderRadius: lerpDouble(borderRadius, other.borderRadius, t)!,
- padding: EdgeInsetsGeometry.lerp(padding, other.padding, t)!,
- compactPadding: EdgeInsetsGeometry.lerp(
- compactPadding,
- other.compactPadding,
- t,
- )!,
- heightThreshold: lerpDouble(heightThreshold, other.heightThreshold, t)!,
- );
- }
-}
diff --git a/lib/core/themes/extensions/metric_card_theme.dart b/lib/core/themes/extensions/metric_card_theme.dart
deleted file mode 100644
index 6f2d660..0000000
--- a/lib/core/themes/extensions/metric_card_theme.dart
+++ /dev/null
@@ -1,106 +0,0 @@
-import 'dart:ui';
-import 'package:flutter/material.dart';
-
-class MetricCardTheme extends ThemeExtension {
- final Color backgroundColor;
- final Color borderColor;
- final double borderRadius;
- final EdgeInsetsGeometry padding;
-
- final TextStyle labelStyle;
- final TextStyle valueStyle;
- final TextStyle valueUnitStyle;
- final TextStyle captionStyle;
- final TextStyle variationTextStyle;
-
- final Color variationPositiveColor;
- final Color variationNegativeColor;
- final Color variationNeutralColor;
-
- final double sparklineStrokeWidth;
- final Color skeletonColor;
-
- const MetricCardTheme({
- required this.backgroundColor,
- required this.borderColor,
- required this.labelStyle,
- required this.valueStyle,
- required this.valueUnitStyle,
- required this.captionStyle,
- required this.variationTextStyle,
- required this.variationPositiveColor,
- required this.variationNegativeColor,
- required this.variationNeutralColor,
- required this.skeletonColor,
- this.borderRadius = 16,
- this.padding = const EdgeInsets.all(16),
- this.sparklineStrokeWidth = 1.6,
- });
-
- @override
- MetricCardTheme copyWith({
- Color? backgroundColor,
- Color? borderColor,
- double? borderRadius,
- EdgeInsetsGeometry? padding,
- TextStyle? labelStyle,
- TextStyle? valueStyle,
- TextStyle? valueUnitStyle,
- TextStyle? captionStyle,
- TextStyle? variationTextStyle,
- Color? variationPositiveColor,
- Color? variationNegativeColor,
- Color? variationNeutralColor,
- Color? skeletonColor,
- double? sparklineStrokeWidth,
- }) {
- return MetricCardTheme(
- backgroundColor: backgroundColor ?? this.backgroundColor,
- borderColor: borderColor ?? this.borderColor,
- borderRadius: borderRadius ?? this.borderRadius,
- padding: padding ?? this.padding,
- labelStyle: labelStyle ?? this.labelStyle,
- valueStyle: valueStyle ?? this.valueStyle,
- valueUnitStyle: valueUnitStyle ?? this.valueUnitStyle,
- captionStyle: captionStyle ?? this.captionStyle,
- variationTextStyle: variationTextStyle ?? this.variationTextStyle,
- variationPositiveColor:
- variationPositiveColor ?? this.variationPositiveColor,
- variationNegativeColor:
- variationNegativeColor ?? this.variationNegativeColor,
- variationNeutralColor:
- variationNeutralColor ?? this.variationNeutralColor,
- skeletonColor: skeletonColor ?? this.skeletonColor,
- sparklineStrokeWidth: sparklineStrokeWidth ?? this.sparklineStrokeWidth,
- );
- }
-
- @override
- MetricCardTheme lerp(
- ThemeExtension? other,
- double t,
- ) {
- if (other is! MetricCardTheme) return this;
- return MetricCardTheme(
- backgroundColor: Color.lerp(backgroundColor, other.backgroundColor, t)!,
- borderColor: Color.lerp(borderColor, other.borderColor, t)!,
- borderRadius: lerpDouble(borderRadius, other.borderRadius, t)!,
- padding: EdgeInsetsGeometry.lerp(padding, other.padding, t)!,
- labelStyle: TextStyle.lerp(labelStyle, other.labelStyle, t)!,
- valueStyle: TextStyle.lerp(valueStyle, other.valueStyle, t)!,
- valueUnitStyle: TextStyle.lerp(valueUnitStyle, other.valueUnitStyle, t)!,
- captionStyle: TextStyle.lerp(captionStyle, other.captionStyle, t)!,
- variationTextStyle:
- TextStyle.lerp(variationTextStyle, other.variationTextStyle, t)!,
- variationPositiveColor: Color.lerp(
- variationPositiveColor, other.variationPositiveColor, t)!,
- variationNegativeColor: Color.lerp(
- variationNegativeColor, other.variationNegativeColor, t)!,
- variationNeutralColor: Color.lerp(
- variationNeutralColor, other.variationNeutralColor, t)!,
- skeletonColor: Color.lerp(skeletonColor, other.skeletonColor, t)!,
- sparklineStrokeWidth:
- lerpDouble(sparklineStrokeWidth, other.sparklineStrokeWidth, t)!,
- );
- }
-}
\ No newline at end of file
diff --git a/lib/core/themes/extensions/options_bottom_forms_theme.dart b/lib/core/themes/extensions/options_bottom_forms_theme.dart
deleted file mode 100644
index 657c5ea..0000000
--- a/lib/core/themes/extensions/options_bottom_forms_theme.dart
+++ /dev/null
@@ -1,51 +0,0 @@
-// core/themes/theme_extensions/options_bottom_forms_theme.dart
-import 'package:flutter/material.dart';
-
-class OptionsBottomFormsTheme extends ThemeExtension {
- final Color calculateButtonBackground;
- final Color calculateButtonForeground;
- final Color actionButtonBackground;
- final Color actionButtonForeground;
- final Color borderButtonColor;
-
- const OptionsBottomFormsTheme({
- required this.calculateButtonBackground,
- required this.calculateButtonForeground,
- required this.actionButtonBackground,
- required this.actionButtonForeground,
- required this.borderButtonColor,
- });
-
- @override
- ThemeExtension copyWith({
- Color? calculateButtonBackground,
- Color? calculateButtonForeground,
- Color? actionButtonBackground,
- Color? actionButtonForeground,
- Color? borderButtonColor
- }) {
- return OptionsBottomFormsTheme(
- calculateButtonBackground: calculateButtonBackground ?? this.calculateButtonBackground,
- calculateButtonForeground: calculateButtonForeground ?? this.calculateButtonForeground,
- actionButtonBackground: actionButtonBackground ?? this.actionButtonBackground,
- actionButtonForeground: actionButtonForeground ?? this.actionButtonForeground,
- borderButtonColor: borderButtonColor ?? this.borderButtonColor,
- );
- }
-
- @override
- ThemeExtension lerp(
- ThemeExtension? other,
- double t,
- ) {
- if (other is! OptionsBottomFormsTheme) return this;
- return OptionsBottomFormsTheme(
- calculateButtonBackground: Color.lerp(calculateButtonBackground, other.calculateButtonBackground, t)!,
- calculateButtonForeground: Color.lerp(calculateButtonForeground, other.calculateButtonForeground, t)!,
- actionButtonBackground: Color.lerp(actionButtonBackground, other.actionButtonBackground, t)!,
- actionButtonForeground: Color.lerp(actionButtonForeground, other.actionButtonForeground, t)!,
- borderButtonColor: Color.lerp(borderButtonColor, other.borderButtonColor, t)!,
-
- );
- }
-}
\ No newline at end of file
diff --git a/lib/core/themes/extensions/page_header_theme.dart b/lib/core/themes/extensions/page_header_theme.dart
deleted file mode 100644
index 3988921..0000000
--- a/lib/core/themes/extensions/page_header_theme.dart
+++ /dev/null
@@ -1,36 +0,0 @@
-import 'package:flutter/material.dart';
-
-class PageHeaderTheme extends ThemeExtension {
- final Color backgroundColor;
- final Color iconColor;
- final TextStyle titleStyle;
-
- const PageHeaderTheme({
- required this.backgroundColor,
- required this.iconColor,
- required this.titleStyle,
- });
-
- @override
- PageHeaderTheme copyWith({
- Color? backgroundColor,
- Color? iconColor,
- TextStyle? titleStyle,
- }) {
- return PageHeaderTheme(
- backgroundColor: backgroundColor ?? this.backgroundColor,
- iconColor: iconColor ?? this.iconColor,
- titleStyle: titleStyle ?? this.titleStyle,
- );
- }
-
- @override
- PageHeaderTheme lerp(ThemeExtension? other, double t) {
- if (other is! PageHeaderTheme) return this;
- return PageHeaderTheme(
- backgroundColor: Color.lerp(backgroundColor, other.backgroundColor, t)!,
- iconColor: Color.lerp(iconColor, other.iconColor, t)!,
- titleStyle: TextStyle.lerp(titleStyle, other.titleStyle, t)!,
- );
- }
-}
\ No newline at end of file
diff --git a/lib/core/themes/extensions/title_widget_theme.dart b/lib/core/themes/extensions/title_widget_theme.dart
deleted file mode 100644
index 46ea7d1..0000000
--- a/lib/core/themes/extensions/title_widget_theme.dart
+++ /dev/null
@@ -1,34 +0,0 @@
-import 'package:flutter/material.dart';
-
-class TitleWidgetTheme extends ThemeExtension {
- final TextStyle titleStyle;
- final TextStyle subtitleStyle;
-
- const TitleWidgetTheme({
- required this.titleStyle,
- required this.subtitleStyle,
- });
-
- @override
- ThemeExtension copyWith({
- TextStyle? titleStyle,
- TextStyle? subtitleStyle,
- }) {
- return TitleWidgetTheme(
- titleStyle: titleStyle ?? this.titleStyle,
- subtitleStyle: subtitleStyle ?? this.subtitleStyle,
- );
- }
-
- @override
- ThemeExtension lerp(
- ThemeExtension? other,
- double t,
- ) {
- if (other is! TitleWidgetTheme) return this;
- return TitleWidgetTheme(
- titleStyle: TextStyle.lerp(titleStyle, other.titleStyle, t)!,
- subtitleStyle: TextStyle.lerp(subtitleStyle, other.subtitleStyle, t)!,
- );
- }
-}
\ No newline at end of file
diff --git a/lib/core/themes/extensions/value_correction_result_theme.dart b/lib/core/themes/extensions/value_correction_result_theme.dart
deleted file mode 100644
index 4010348..0000000
--- a/lib/core/themes/extensions/value_correction_result_theme.dart
+++ /dev/null
@@ -1,102 +0,0 @@
-import 'package:flutter/material.dart';
-
-class ValueCorrectionResultTheme extends ThemeExtension {
- final Color cardBackgroundColor;
- final Color cardBorderColor;
- final Color spotlightBackground;
- final Color spotlightBorderColor;
- final Color variationPositiveColor;
- final Color shareButtonColor;
- final Color editButtonColor;
- final TextStyle titleStyle;
- final TextStyle subtitleStyle;
- final TextStyle spotlightLabelStyle;
- final TextStyle spotlightValueStyle;
- final TextStyle spotlightVariationStyle;
- final TextStyle detailLabelStyle;
- final TextStyle detailValueStyle;
- final TextStyle linkStyle;
-
- const ValueCorrectionResultTheme({
- required this.cardBackgroundColor,
- required this.cardBorderColor,
- required this.spotlightBackground,
- required this.spotlightBorderColor,
- required this.variationPositiveColor,
- required this.shareButtonColor,
- required this.editButtonColor,
- required this.titleStyle,
- required this.subtitleStyle,
- required this.spotlightLabelStyle,
- required this.spotlightValueStyle,
- required this.spotlightVariationStyle,
- required this.detailLabelStyle,
- required this.detailValueStyle,
- required this.linkStyle,
- });
-
- @override
- ThemeExtension copyWith({
- Color? cardBackgroundColor,
- Color? cardBorderColor,
- Color? spotlightBackground,
- Color? spotlightBorderColor,
- Color? variationPositiveColor,
- Color? shareButtonColor,
- Color? editButtonColor,
- TextStyle? titleStyle,
- TextStyle? subtitleStyle,
- TextStyle? spotlightLabelStyle,
- TextStyle? spotlightValueStyle,
- TextStyle? spotlightVariationStyle,
- TextStyle? detailLabelStyle,
- TextStyle? detailValueStyle,
- TextStyle? linkStyle,
- }) {
- return ValueCorrectionResultTheme(
- cardBackgroundColor: cardBackgroundColor ?? this.cardBackgroundColor,
- cardBorderColor: cardBorderColor ?? this.cardBorderColor,
- spotlightBackground: spotlightBackground ?? this.spotlightBackground,
- spotlightBorderColor: spotlightBorderColor ?? this.spotlightBorderColor,
- variationPositiveColor:
- variationPositiveColor ?? this.variationPositiveColor,
- shareButtonColor: shareButtonColor ?? this.shareButtonColor,
- editButtonColor: editButtonColor ?? this.editButtonColor,
- titleStyle: titleStyle ?? this.titleStyle,
- subtitleStyle: subtitleStyle ?? this.subtitleStyle,
- spotlightLabelStyle: spotlightLabelStyle ?? this.spotlightLabelStyle,
- spotlightValueStyle: spotlightValueStyle ?? this.spotlightValueStyle,
- spotlightVariationStyle:
- spotlightVariationStyle ?? this.spotlightVariationStyle,
- detailLabelStyle: detailLabelStyle ?? this.detailLabelStyle,
- detailValueStyle: detailValueStyle ?? this.detailValueStyle,
- linkStyle: linkStyle ?? this.linkStyle,
- );
- }
-
- @override
- ThemeExtension lerp(
- ThemeExtension? other,
- double t,
- ) {
- if (other is! ValueCorrectionResultTheme) return this;
-
- return ValueCorrectionResultTheme(
- cardBackgroundColor: Color.lerp(cardBackgroundColor, other.cardBackgroundColor, t)!,
- cardBorderColor: Color.lerp(cardBorderColor, other.cardBorderColor, t)!,
- spotlightBackground: Color.lerp(spotlightBackground, other.spotlightBackground, t)!,
- spotlightBorderColor: Color.lerp(spotlightBorderColor, other.spotlightBorderColor, t)!,
- variationPositiveColor: Color.lerp(variationPositiveColor, other.variationPositiveColor, t)!,
- shareButtonColor: Color.lerp(shareButtonColor, other.shareButtonColor, t)!,
- editButtonColor: Color.lerp(editButtonColor, other.editButtonColor, t)!,
- titleStyle: TextStyle.lerp(titleStyle, other.titleStyle, t)!,
- subtitleStyle: TextStyle.lerp(subtitleStyle, other.subtitleStyle, t)!,
- spotlightLabelStyle: TextStyle.lerp(spotlightLabelStyle, other.spotlightLabelStyle, t)!,
- spotlightValueStyle: TextStyle.lerp(spotlightValueStyle, other.spotlightValueStyle, t)!,
- spotlightVariationStyle: TextStyle.lerp(spotlightVariationStyle, other.spotlightVariationStyle, t)!,
- detailLabelStyle: TextStyle.lerp(detailLabelStyle, other.detailLabelStyle, t)!,
- detailValueStyle: TextStyle.lerp(detailValueStyle, other.detailValueStyle, t)!,
- linkStyle: TextStyle.lerp(linkStyle, other.linkStyle, t)!,
- );
- }
-}
diff --git a/lib/core/themes/extensions/warning_banner_theme.dart b/lib/core/themes/extensions/warning_banner_theme.dart
deleted file mode 100644
index 99741a4..0000000
--- a/lib/core/themes/extensions/warning_banner_theme.dart
+++ /dev/null
@@ -1,66 +0,0 @@
-import 'package:flutter/material.dart';
-
-class WarningBannerTheme extends ThemeExtension {
- final Color backgroundColor;
- final Color borderColor;
- final Color iconColor;
- final TextStyle textStyle;
- final EdgeInsetsGeometry padding;
- final double borderRadius;
- final double iconSize;
- final double iconTextSpacing;
-
- const WarningBannerTheme({
- required this.backgroundColor,
- required this.borderColor,
- required this.iconColor,
- required this.textStyle,
- this.padding = const EdgeInsets.all(12),
- this.borderRadius = 12,
- this.iconSize = 18,
- this.iconTextSpacing = 10,
- });
-
- @override
- WarningBannerTheme copyWith({
- Color? backgroundColor,
- Color? borderColor,
- Color? iconColor,
- TextStyle? textStyle,
- EdgeInsetsGeometry? padding,
- double? borderRadius,
- double? iconSize,
- double? iconTextSpacing,
- }) {
- return WarningBannerTheme(
- backgroundColor: backgroundColor ?? this.backgroundColor,
- borderColor: borderColor ?? this.borderColor,
- iconColor: iconColor ?? this.iconColor,
- textStyle: textStyle ?? this.textStyle,
- padding: padding ?? this.padding,
- borderRadius: borderRadius ?? this.borderRadius,
- iconSize: iconSize ?? this.iconSize,
- iconTextSpacing: iconTextSpacing ?? this.iconTextSpacing,
- );
- }
-
- @override
- WarningBannerTheme lerp(
- covariant ThemeExtension? other,
- double t,
- ) {
- if (other is! WarningBannerTheme) return this;
-
- return WarningBannerTheme(
- backgroundColor: Color.lerp(backgroundColor, other.backgroundColor, t)!,
- borderColor: Color.lerp(borderColor, other.borderColor, t)!,
- iconColor: Color.lerp(iconColor, other.iconColor, t)!,
- textStyle: TextStyle.lerp(textStyle, other.textStyle, t)!,
- padding: EdgeInsetsGeometry.lerp(padding, other.padding, t)!,
- borderRadius: borderRadius + (other.borderRadius - borderRadius) * t,
- iconSize: iconSize + (other.iconSize - iconSize) * t,
- iconTextSpacing:
- iconTextSpacing + (other.iconTextSpacing - iconTextSpacing) * t,
- );
- }
-}
diff --git a/lib/core/themes/radius.dart b/lib/core/themes/radius.dart
new file mode 100644
index 0000000..ef114dd
--- /dev/null
+++ b/lib/core/themes/radius.dart
@@ -0,0 +1,7 @@
+abstract class AppRadius {
+ static const double button = 28;
+ static const double sheet = 28;
+ static const double iconTile = 12;
+ static const double search = 14;
+ static const double pill = 999;
+}
\ No newline at end of file
diff --git a/lib/core/themes/spacing.dart b/lib/core/themes/spacing.dart
index 9fee8bd..38c5d0c 100644
--- a/lib/core/themes/spacing.dart
+++ b/lib/core/themes/spacing.dart
@@ -7,6 +7,13 @@ abstract class AppSpacing {
static const double lg = 24;
static const double xl = 32;
static const double xxl = 48;
+
+ // Vibrante minimalista layout tokens.
+ static const double screen = 24;
+ static const double rowHeight = 58;
+ static const double rowHeightWithSecondLine = 64;
+ static const double homeRowHeight = 72;
+ static const double sectionGap = 26;
// Padding padrão de tela
static const EdgeInsets screenPadding = EdgeInsets.all(md);
diff --git a/lib/core/themes/text_styles.dart b/lib/core/themes/text_styles.dart
index 87433e2..abef221 100644
--- a/lib/core/themes/text_styles.dart
+++ b/lib/core/themes/text_styles.dart
@@ -1,120 +1,123 @@
import 'package:flutter/material.dart';
-import 'package:google_fonts/google_fonts.dart';
-import 'color_tokens.dart';
class AppTextStyles {
- static final TextStyle _base = GoogleFonts.manrope();
- static final TextStyle _baseWhite = _base.copyWith(color: Colors.white);
+ static const String manropeFamily = 'Manrope';
+ static const String bricolageGrotesqueFamily = 'Bricolage Grotesque';
- // Headlines — mantidos, ainda usados pela TextTheme padrão do Material
- // (dialogs, snackbars etc.) mesmo com o header customizado nas telas.
- static final TextStyle headlineLarge = _base.copyWith(
- fontSize: 32,
- fontWeight: FontWeight.bold,
- letterSpacing: -0.5,
+ // Estilos de destaque e componentes da direção Vibrante minimalista.
+ // As cores são definidas pelo componente ou tema que utiliza cada estilo.
+ static final TextStyle bandStatement = const TextStyle(
+ fontFamily: bricolageGrotesqueFamily,
+ fontSize: 48,
+ fontWeight: FontWeight.w800,
+ letterSpacing: -1.8,
+ );
+ static final TextStyle screenTitle = const TextStyle(
+ fontFamily: bricolageGrotesqueFamily,
+ fontSize: 28,
+ fontWeight: FontWeight.w800,
+ letterSpacing: -0.9,
+ );
+ static final TextStyle sheetTitle = const TextStyle(
+ fontFamily: bricolageGrotesqueFamily,
+ fontSize: 28,
+ fontWeight: FontWeight.w800,
+ letterSpacing: -0.8,
+ );
+ static final TextStyle exampleValue = const TextStyle(
+ fontFamily: bricolageGrotesqueFamily,
+ fontSize: 30,
+ fontWeight: FontWeight.w800,
+ letterSpacing: -0.8,
);
- static final TextStyle headlineMedium = _baseWhite.copyWith(
- fontSize: 18,
- fontWeight: FontWeight.bold,
+ static final TextStyle wordmark = const TextStyle(
+ fontFamily: bricolageGrotesqueFamily,
+ fontSize: 21,
+ fontWeight: FontWeight.w800,
+ letterSpacing: -0.5,
);
- // Body — mantidos sem alteração, `bodyLarge` já é o tamanho correto
- // usado nos valores de input pelo InputForms.
- static final TextStyle bodyLarge = _base.copyWith(fontSize: 16);
- static final TextStyle bodyMedium = _base.copyWith(fontSize: 14);
- static final TextStyle labelLarge = _base.copyWith(
- fontSize: 14,
+ static final TextStyle bandKicker = const TextStyle(
+ fontFamily: manropeFamily,
+ fontSize: 15,
+ fontWeight: FontWeight.w600,
+ );
+ static final TextStyle bandSub = const TextStyle(
+ fontFamily: manropeFamily,
+ fontSize: 15,
fontWeight: FontWeight.w500,
- letterSpacing: 0.5,
);
-
- // Componentes gerais — mantidos. errorBannerText já usa ColorTokens.error,
- // só depende da cor em si ser atualizada (ColorTokens), não do estilo.
- static final TextStyle helpCardMessage = _base.copyWith(
- fontSize: 14,
- color: ColorTokens.textPrimary,
- fontWeight: FontWeight.w400,
- );
- static final TextStyle inputHelperText = _base.copyWith(
- fontSize: 12,
- color: ColorTokens.textHint,
+ static final TextStyle headerTitle = const TextStyle(
+ fontFamily: manropeFamily,
+ fontSize: 16,
+ fontWeight: FontWeight.w700,
);
- static final TextStyle errorBannerText = _base.copyWith(
- fontSize: 13,
+ static final TextStyle rowLabel = const TextStyle(
+ fontFamily: manropeFamily,
+ fontSize: 16,
fontWeight: FontWeight.w500,
- color: ColorTokens.error,
);
-
- // Card de métrica (Selic) — NOVO. Substitui highlightCardTitle/
- // highlightCardSubtitle/highlightCardButton, que eram específicos do
- // HighlightCard antigo (removido). Cor não é fixada aqui porque muda
- // conforme o estado (amber padrão, verde/vermelho em variação futura);
- // aplicar via .copyWith(color: ...) no componente.
- static final TextStyle metricLabel = _base.copyWith(
- fontSize: 12,
- color: ColorTokens.textSecondary,
+ static final TextStyle rowValue = TextStyle(
+ fontFamily: manropeFamily,
+ fontSize: 17,
+ fontWeight: FontWeight.w700,
+ fontFeatures: const [FontFeature.tabularFigures()],
);
- static final TextStyle metricValue = _base.copyWith(
- fontSize: 30,
+ static final TextStyle rowUnit = const TextStyle(
+ fontFamily: manropeFamily,
+ fontSize: 14,
fontWeight: FontWeight.w500,
- letterSpacing: -0.6,
- fontFeatures: const [FontFeature.tabularFigures()],
);
- static final TextStyle metricCaption = _base.copyWith(
- fontSize: 11,
- color: ColorTokens.textHint,
+ static final TextStyle rowState = const TextStyle(
+ fontFamily: manropeFamily,
+ fontSize: 12,
+ fontWeight: FontWeight.w700,
);
-
- // Badge de estado do campo ("vazio" / "calculado" / "resultado") — NOVO.
- // Sem cor fixa: cada estado (amber/verde/vermelho) aplica a própria cor.
- static final TextStyle stateBadge = _base.copyWith(
- fontSize: 11,
+ static final TextStyle listTitle = const TextStyle(
+ fontFamily: manropeFamily,
+ fontSize: 17,
+ fontWeight: FontWeight.w700,
+ );
+ static final TextStyle listDesc = const TextStyle(
+ fontFamily: manropeFamily,
+ fontSize: 13,
fontWeight: FontWeight.w500,
);
-
- // MenuCard
- static final TextStyle menuCardTitle = _baseWhite.copyWith(
+ static final TextStyle sectionLabel = const TextStyle(
+ fontFamily: manropeFamily,
fontSize: 13,
- fontWeight: FontWeight.bold,
+ fontWeight: FontWeight.w600,
);
- static final TextStyle menuCardTitleCompact = _baseWhite.copyWith(
- fontSize: 11,
- fontWeight: FontWeight.bold,
+ static final TextStyle detailLabel = const TextStyle(
+ fontFamily: manropeFamily,
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
);
- static final TextStyle menuCardDescription = _baseWhite.copyWith(
- fontSize: 10,
- height: 1.1,
+ static final TextStyle detailValue = TextStyle(
+ fontFamily: manropeFamily,
+ fontSize: 15,
+ fontWeight: FontWeight.w700,
+ fontFeatures: const [FontFeature.tabularFigures()],
);
- // Corrigido: estava em 9px, abaixo do mínimo legível (11px).
- static final TextStyle menuCardDescriptionCompact = _baseWhite.copyWith(
- fontSize: 11,
- height: 1.1,
+ static final TextStyle button = const TextStyle(
+ fontFamily: manropeFamily,
+ fontSize: 16,
+ fontWeight: FontWeight.w800,
);
-
- // MenuCard — variante featured (tile full-width, texto um pouco maior
- // por ter mais espaço horizontal disponível). NOVO.
- static final TextStyle menuCardTitleFeatured = _baseWhite.copyWith(
- fontSize: 14,
- fontWeight: FontWeight.bold,
+ static final TextStyle pill = const TextStyle(
+ fontFamily: manropeFamily,
+ fontSize: 13,
+ fontWeight: FontWeight.w800,
);
- static final TextStyle menuCardDescriptionFeatured = _base.copyWith(
- fontSize: 12,
- color: ColorTokens.textSecondary,
+ static final TextStyle body = const TextStyle(
+ fontFamily: manropeFamily,
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ height: 1.5,
);
-
- // Home
- static final TextStyle appName = _baseWhite.copyWith(
- fontSize: 20,
- fontWeight: FontWeight.bold,
- );
- // Cor movida pra dentro do estilo (era aplicada com .withValues(alpha: 0.4)
- // no call site em home_page.dart) — evita repetir o ajuste de opacidade
- // toda vez que o label for usado.
- static final TextStyle sectionHeader = _base.copyWith(
+ static final TextStyle caption = const TextStyle(
+ fontFamily: manropeFamily,
fontSize: 12,
- fontWeight: FontWeight.bold,
- letterSpacing: 1.2,
- color: ColorTokens.textSecondary,
+ fontWeight: FontWeight.w500,
);
- static final TextStyle historyItem = _baseWhite.copyWith(fontSize: 14);
-}
\ No newline at end of file
+}
diff --git a/lib/core/themes/theme_cubit.dart b/lib/core/themes/theme_cubit.dart
new file mode 100644
index 0000000..83d30a9
--- /dev/null
+++ b/lib/core/themes/theme_cubit.dart
@@ -0,0 +1,31 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+/// Guarda e persiste o [ThemeMode] escolhido pelo usuário.
+class ThemeCubit extends Cubit {
+ static const storageKey = 'theme_mode';
+
+ static const initialMode = ThemeMode.system;
+
+ final SharedPreferences _preferences;
+
+ ThemeCubit(this._preferences) : super(_read(_preferences));
+
+ static ThemeMode _read(SharedPreferences preferences) {
+ final stored = preferences.getString(storageKey);
+ return ThemeMode.values.firstWhere(
+ (mode) => mode.name == stored,
+ orElse: () => initialMode,
+ );
+ }
+
+ Future setMode(ThemeMode mode) async {
+ emit(mode);
+ await _preferences.setString(storageKey, mode.name);
+ }
+
+ /// Alterna entre claro e escuro a partir do brilho que está na tela.
+ Future toggle(Brightness current) =>
+ setMode(current == Brightness.dark ? ThemeMode.light : ThemeMode.dark);
+}
diff --git a/lib/core/utils/br_number_format.dart b/lib/core/utils/br_number_format.dart
new file mode 100644
index 0000000..c17809e
--- /dev/null
+++ b/lib/core/utils/br_number_format.dart
@@ -0,0 +1,24 @@
+import 'dart:math';
+
+import 'package:intl/intl.dart';
+
+final _decimal = NumberFormat('#,##0.00', 'pt_BR');
+
+/// Arredonda para centavos e devolve o valor em centavos inteiros, para as
+/// contas derivadas fecharem com o que a pessoa vê na tela.
+int toCents(double value) => (value * 100).round();
+
+/// "R$ 2.836,80", com espaço não separável entre o símbolo e o número.
+/// Negativos ficam com o sinal antes do símbolo: "-R$ 10,00".
+String formatCents(int cents) {
+ final sign = cents < 0 ? '-' : '';
+ return '${sign}R\$\u00A0${_decimal.format(cents.abs() / 100)}';
+}
+
+/// "26,82%".
+String formatPercent(double percent) => '${_decimal.format(percent)}%';
+
+/// Taxa anual equivalente, em %, a partir da taxa mensal em %:
+/// (1 + i)¹² − 1.
+double annualEquivalentRate(double monthlyPercent) =>
+ (pow(1 + monthlyPercent / 100, 12) - 1) * 100;
diff --git a/lib/core/utils/share_text.dart b/lib/core/utils/share_text.dart
new file mode 100644
index 0000000..5f09a32
--- /dev/null
+++ b/lib/core/utils/share_text.dart
@@ -0,0 +1,24 @@
+import 'package:real_calc/core/widgets/calculator_band/band_statement.dart';
+
+const shareDisclaimer = 'Simulação feita no RealCalc, sem valor oficial.';
+
+String shareText({
+ required String calculator,
+ required BandStatement statement,
+ required List<(String, String)> lines,
+}) {
+ final headline = [
+ ?statement.kicker,
+ statement.isMoney ? 'R\$ ${statement.value}' : statement.value,
+ ].join(' ');
+
+ return [
+ 'RealCalc · $calculator',
+ headline,
+ ?statement.sub,
+ '',
+ for (final (label, value) in lines) '$label: $value',
+ '',
+ shareDisclaimer,
+ ].join('\n').replaceAll(String.fromCharCode(0xA0), ' ');
+}
diff --git a/lib/core/widgets/app_icon.dart b/lib/core/widgets/app_icon.dart
new file mode 100644
index 0000000..30ef6fc
--- /dev/null
+++ b/lib/core/widgets/app_icon.dart
@@ -0,0 +1,55 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+
+abstract class AppIcons {
+ static const _dir = 'assets/icons';
+
+ static const alert = '$_dir/alert.svg';
+ static const arrowDown = '$_dir/arrow_down.svg';
+ static const arrowUp = '$_dir/arrow_up.svg';
+ static const back = '$_dir/back.svg';
+ static const bank = '$_dir/bank.svg';
+ static const check = '$_dir/check.svg';
+ static const chevronDown = '$_dir/chevron_down.svg';
+ static const chevronRight = '$_dir/chevron_right.svg';
+ static const close = '$_dir/close.svg';
+ static const cycle = '$_dir/cycle.svg';
+ static const help = '$_dir/help.svg';
+ static const moon = '$_dir/moon.svg';
+ static const piggyBank = '$_dir/piggy_bank.svg';
+ static const reset = '$_dir/reset.svg';
+ static const search = '$_dir/search.svg';
+ static const share = '$_dir/share.svg';
+ static const sun = '$_dir/sun.svg';
+ static const trendingUp = '$_dir/trending_up.svg';
+ static const wifiOff = '$_dir/wifi_off.svg';
+}
+
+/// Ícone de `assets/icons`; sem cor ou tamanho, segue o [IconTheme], como o
+/// [Icon], e por isso funciona dentro de [IconButton].
+class AppIcon extends StatelessWidget {
+ final String asset;
+ final double? size;
+ final Color? color;
+
+ const AppIcon(this.asset, {super.key, this.size, this.color});
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = IconTheme.of(context);
+ final size = this.size ?? theme.size ?? 24;
+ final color = this.color ?? theme.color ?? Colors.black;
+
+ // Como o Icon: em restrições justas, fica centralizado no tamanho pedido.
+ return Center(
+ widthFactor: 1,
+ heightFactor: 1,
+ child: SvgPicture.asset(
+ asset,
+ width: size,
+ height: size,
+ colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
+ ),
+ );
+ }
+}
diff --git a/lib/core/widgets/app_logo.dart b/lib/core/widgets/app_logo.dart
new file mode 100644
index 0000000..4042657
--- /dev/null
+++ b/lib/core/widgets/app_logo.dart
@@ -0,0 +1,56 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:real_calc/core/themes/app_colors.dart';
+import 'package:real_calc/core/themes/app_image.dart';
+import 'package:real_calc/core/themes/text_styles.dart';
+
+class AppLogo extends StatelessWidget {
+ static const double smallSymbolMaxHeight = 24;
+
+ final double symbolHeight;
+ final bool showWordmark;
+
+ const AppLogo({super.key, this.symbolHeight = 26, this.showWordmark = true});
+
+ @override
+ Widget build(BuildContext context) {
+ final amber = AppColors.of(context).brandAmber;
+ final symbol = SvgPicture.asset(
+ symbolHeight <= smallSymbolMaxHeight
+ ? AppImage.appLogoSmall
+ : AppImage.appLogo,
+ height: symbolHeight,
+ colorFilter: ColorFilter.mode(amber, BlendMode.srcIn),
+ );
+
+ return Semantics(
+ label: 'RealCalc',
+ image: true,
+ excludeSemantics: true,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ symbol,
+ if (showWordmark) ...[
+ const SizedBox(width: 10),
+ Text.rich(
+ TextSpan(
+ children: [
+ const TextSpan(
+ text: 'Real',
+ style: TextStyle(color: Colors.white),
+ ),
+ TextSpan(
+ text: 'Calc',
+ style: TextStyle(color: amber),
+ ),
+ ],
+ ),
+ style: AppTextStyles.wordmark,
+ ),
+ ],
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/core/widgets/app_sheet.dart b/lib/core/widgets/app_sheet.dart
new file mode 100644
index 0000000..263cc80
--- /dev/null
+++ b/lib/core/widgets/app_sheet.dart
@@ -0,0 +1,153 @@
+import 'package:flutter/material.dart';
+import 'package:real_calc/core/widgets/app_icon.dart';
+import 'package:real_calc/core/themes/app_colors.dart';
+import 'package:real_calc/core/themes/calculator_palette.dart';
+import 'package:real_calc/core/themes/calculator_scope.dart';
+import 'package:real_calc/core/themes/radius.dart';
+import 'package:real_calc/core/themes/spacing.dart';
+import 'package:real_calc/core/themes/text_styles.dart';
+
+Future showAppSheet({
+ required BuildContext context,
+ required String title,
+ required WidgetBuilder bodyBuilder,
+ WidgetBuilder? footerBuilder,
+ CalculatorKind? kind,
+ bool scrollable = true,
+}) {
+ final colors = AppColors.of(context);
+
+ return showModalBottomSheet(
+ context: context,
+ isScrollControlled: true,
+ backgroundColor: colors.sheet,
+ barrierColor: colors.scrim,
+ clipBehavior: Clip.antiAlias,
+ shape: const RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(
+ top: Radius.circular(AppRadius.sheet),
+ ),
+ ),
+ constraints: BoxConstraints(
+ maxHeight: MediaQuery.sizeOf(context).height - AppSheet.topGap,
+ ),
+ builder: (context) {
+ final sheet = AppSheet(
+ title: title,
+ body: Builder(builder: bodyBuilder),
+ footer: footerBuilder == null ? null : Builder(builder: footerBuilder),
+ scrollable: scrollable,
+ );
+ return kind == null ? sheet : CalculatorScope(kind: kind, child: sheet);
+ },
+ );
+}
+
+class AppSheet extends StatelessWidget {
+ static const double topGap = 96;
+ static const closeLabel = 'Fechar';
+
+ final String title;
+ final Widget body;
+ final Widget? footer;
+
+ /// Com `false`, o corpo cuida da própria rolagem e do respiro inferior.
+ final bool scrollable;
+
+ const AppSheet({
+ super.key,
+ required this.title,
+ required this.body,
+ this.footer,
+ this.scrollable = true,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final colors = AppColors.of(context);
+ final bottomInset = MediaQuery.viewPaddingOf(context).bottom;
+ final keyboard = MediaQuery.viewInsetsOf(context).bottom;
+
+ final sheet = Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(top: 10),
+ child: Center(
+ child: Container(
+ width: 40,
+ height: 4,
+ decoration: BoxDecoration(
+ color: colors.textHint.withValues(alpha: 0.5),
+ borderRadius: BorderRadius.circular(2),
+ ),
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.fromLTRB(AppSpacing.screen, 8, 12, 8),
+ child: Row(
+ children: [
+ Expanded(
+ child: Semantics(
+ header: true,
+ child: Text(
+ title,
+ style: AppTextStyles.sheetTitle.copyWith(
+ color: colors.textPrimary,
+ ),
+ ),
+ ),
+ ),
+ MergeSemantics(
+ child: Semantics(
+ label: closeLabel,
+ child: SizedBox.square(
+ dimension: 44,
+ child: IconButton(
+ onPressed: () => Navigator.of(context).maybePop(),
+ padding: EdgeInsets.zero,
+ color: colors.textSecondary,
+ icon: const AppIcon(AppIcons.close, size: 22),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ Flexible(
+ child: scrollable
+ ? SingleChildScrollView(
+ padding: EdgeInsets.fromLTRB(
+ AppSpacing.screen,
+ AppSpacing.sm,
+ AppSpacing.screen,
+ footer == null
+ ? AppSpacing.lg + bottomInset
+ : AppSpacing.md,
+ ),
+ child: body,
+ )
+ : body,
+ ),
+ if (footer != null)
+ Padding(
+ padding: EdgeInsets.fromLTRB(
+ AppSpacing.screen,
+ AppSpacing.sm,
+ AppSpacing.screen,
+ AppSpacing.md + bottomInset,
+ ),
+ child: footer,
+ ),
+ ],
+ );
+
+ return Padding(
+ padding: EdgeInsets.only(bottom: keyboard),
+ child: sheet,
+ );
+ }
+}
diff --git a/lib/core/widgets/bottom_action_bar.dart b/lib/core/widgets/bottom_action_bar.dart
new file mode 100644
index 0000000..1e9c510
--- /dev/null
+++ b/lib/core/widgets/bottom_action_bar.dart
@@ -0,0 +1,131 @@
+import 'package:flutter/material.dart';
+import 'package:real_calc/core/widgets/app_icon.dart';
+import 'package:real_calc/core/themes/app_colors.dart';
+import 'package:real_calc/core/themes/calculator_scope.dart';
+import 'package:real_calc/core/themes/text_styles.dart';
+
+/// Deve ficar no `body`, abaixo da rolagem, e não em `bottomNavigationBar`,
+/// para subir junto com o teclado.
+class BottomActionBar extends StatelessWidget {
+ static const double buttonHeight = 56;
+ static const EdgeInsets padding = EdgeInsets.fromLTRB(24, 12, 24, 30);
+ static const shareLabel = 'Compartilhar resultado';
+
+ final String primaryLabel;
+ final VoidCallback? onPrimary;
+ final bool isLoading;
+
+ final ValueChanged? onShare;
+
+ const BottomActionBar({
+ super.key,
+ required this.primaryLabel,
+ required this.onPrimary,
+ this.onShare,
+ this.isLoading = false,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final colors = AppColors.of(context);
+ final scheme = Theme.of(context).colorScheme;
+ final palette = CalculatorScope.maybeOf(context);
+ final solid = palette?.solid ?? scheme.primary;
+ final onSolid = palette?.onSolid ?? scheme.onPrimary;
+
+ return ColoredBox(
+ color: colors.background,
+ child: SafeArea(
+ top: false,
+ minimum: padding,
+ child: Row(
+ children: [
+ Opacity(
+ opacity: onShare == null ? 0.4 : 1,
+ child: SizedBox.square(
+ dimension: buttonHeight,
+ child: Tooltip(
+ message: shareLabel,
+ excludeFromSemantics: true,
+ child: Semantics(
+ label: shareLabel,
+ button: true,
+ enabled: onShare != null,
+ child: Builder(
+ builder: (buttonContext) => OutlinedButton(
+ onPressed: onShare == null
+ ? null
+ : () => onShare!(_globalRect(buttonContext)),
+ style: OutlinedButton.styleFrom(
+ padding: EdgeInsets.zero,
+ shape: const CircleBorder(),
+ side: BorderSide(color: colors.hairline, width: 1.5),
+ foregroundColor: colors.textPrimary,
+ disabledForegroundColor: colors.textPrimary,
+ ),
+ child: const AppIcon(AppIcons.share, size: 22),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: SizedBox(
+ height: buttonHeight,
+ child: FilledButton(
+ onPressed: isLoading ? null : onPrimary,
+ style: FilledButton.styleFrom(
+ backgroundColor: solid,
+ foregroundColor: onSolid,
+ disabledBackgroundColor: isLoading
+ ? solid
+ : solid.withValues(alpha: 0.4),
+ disabledForegroundColor: onSolid,
+ shape: const StadiumBorder(),
+ textStyle: AppTextStyles.button,
+ ),
+ child: isLoading
+ ? Semantics(
+ label: primaryLabel,
+ child: SizedBox.square(
+ dimension: 22,
+ child: CircularProgressIndicator(
+ strokeWidth: 2.4,
+ color: onSolid,
+ ),
+ ),
+ )
+ : Text(primaryLabel),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+ScaffoldFeatureController
+showFieldsClearedSnackBar(
+ BuildContext context, {
+ required VoidCallback onUndo,
+}) {
+ final messenger = ScaffoldMessenger.of(context)..hideCurrentSnackBar();
+ return messenger.showSnackBar(
+ SnackBar(
+ content: const Text('Campos limpos'),
+ duration: const Duration(seconds: 4),
+ // Com ação, o SnackBar não some sozinho por padrão.
+ persist: false,
+ action: SnackBarAction(label: 'Desfazer', onPressed: onUndo),
+ ),
+ );
+}
+
+Rect _globalRect(BuildContext context) {
+ final box = context.findRenderObject()! as RenderBox;
+ return box.localToGlobal(Offset.zero) & box.size;
+}
diff --git a/lib/core/widgets/calculator_band/band_statement.dart b/lib/core/widgets/calculator_band/band_statement.dart
new file mode 100644
index 0000000..ca58f84
--- /dev/null
+++ b/lib/core/widgets/calculator_band/band_statement.dart
@@ -0,0 +1,30 @@
+class BandStatement {
+ final String? kicker;
+
+ final String value;
+
+ final bool isMoney;
+
+ final String? sub;
+
+ const BandStatement({
+ this.kicker,
+ required this.value,
+ this.isMoney = false,
+ this.sub,
+ });
+
+ String get semanticsLabel =>
+ [?kicker, isMoney ? 'R\$ $value' : value, ?sub].join(' ');
+
+ @override
+ bool operator ==(Object other) =>
+ other is BandStatement &&
+ other.kicker == kicker &&
+ other.value == value &&
+ other.isMoney == isMoney &&
+ other.sub == sub;
+
+ @override
+ int get hashCode => Object.hash(kicker, value, isMoney, sub);
+}
diff --git a/lib/core/widgets/calculator_band/calculator_band.dart b/lib/core/widgets/calculator_band/calculator_band.dart
new file mode 100644
index 0000000..f01bad9
--- /dev/null
+++ b/lib/core/widgets/calculator_band/calculator_band.dart
@@ -0,0 +1,255 @@
+import 'dart:math' as math;
+
+import 'package:flutter/material.dart';
+import 'package:real_calc/core/widgets/app_icon.dart';
+import 'package:flutter/services.dart';
+import 'package:real_calc/core/themes/calculator_palette.dart';
+import 'package:real_calc/core/themes/calculator_scope.dart';
+import 'package:real_calc/core/themes/spacing.dart';
+import 'package:real_calc/core/themes/text_styles.dart';
+
+import 'band_statement.dart';
+
+class CalculatorBand extends StatelessWidget {
+ static const double iconTouchSize = 44;
+ static const double moneySymbolSize = 22;
+ static const double minValueSize = 36;
+
+ static const backLabel = 'Voltar';
+ static const helpLabel = 'Como funciona';
+ static const clearLabel = 'Limpar campos';
+
+ static const statusBarStyle = SystemUiOverlayStyle(
+ statusBarColor: Colors.transparent,
+ statusBarIconBrightness: Brightness.light,
+ statusBarBrightness: Brightness.dark,
+ );
+
+ final String title;
+ final BandStatement statement;
+ final VoidCallback? onBack;
+ final VoidCallback? onHelp;
+ final VoidCallback? onClear;
+
+ const CalculatorBand({
+ super.key,
+ required this.title,
+ required this.statement,
+ this.onBack,
+ this.onHelp,
+ this.onClear,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final palette =
+ CalculatorScope.maybeOf(context) ??
+ CalculatorPalette(
+ solid: Theme.of(context).colorScheme.primary,
+ onSolid: Theme.of(context).colorScheme.onPrimary,
+ text: Theme.of(context).colorScheme.primary,
+ );
+ final onSolid = palette.onSolid;
+
+ return AnnotatedRegion(
+ value: statusBarStyle,
+ child: ColoredBox(
+ color: palette.solid,
+ child: SafeArea(
+ bottom: false,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 6),
+ child: Row(
+ children: [
+ _BandIconButton(
+ icon: AppIcons.back,
+ label: backLabel,
+ color: onSolid,
+ onPressed:
+ onBack ?? () => Navigator.of(context).maybePop(),
+ ),
+ const SizedBox(width: 2),
+ Expanded(
+ child: Text(
+ title,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: AppTextStyles.headerTitle.copyWith(
+ color: onSolid,
+ ),
+ ),
+ ),
+ if (onHelp != null)
+ _BandIconButton(
+ icon: AppIcons.help,
+ label: helpLabel,
+ color: onSolid,
+ onPressed: onHelp!,
+ ),
+ if (onClear != null)
+ _BandIconButton(
+ icon: AppIcons.reset,
+ label: clearLabel,
+ color: onSolid,
+ onPressed: onClear!,
+ ),
+ ],
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.fromLTRB(
+ AppSpacing.screen,
+ AppSpacing.sm,
+ AppSpacing.screen,
+ AppSpacing.lg + 4,
+ ),
+ child: _Statement(statement: statement, color: onSolid),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _Statement extends StatelessWidget {
+ final BandStatement statement;
+ final Color color;
+
+ const _Statement({required this.statement, required this.color});
+
+ @override
+ Widget build(BuildContext context) {
+ final soft = color.withValues(alpha: 0.82);
+
+ return Semantics(
+ container: true,
+ liveRegion: true,
+ label: statement.semanticsLabel,
+ child: ExcludeSemantics(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (statement.kicker != null)
+ Text(
+ statement.kicker!,
+ style: AppTextStyles.bandKicker.copyWith(color: soft),
+ ),
+ _StatementValue(statement: statement, color: color),
+ if (statement.sub != null)
+ Text(
+ statement.sub!,
+ style: AppTextStyles.bandSub.copyWith(color: soft),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _StatementValue extends StatelessWidget {
+ final BandStatement statement;
+ final Color color;
+
+ const _StatementValue({required this.statement, required this.color});
+
+ @override
+ Widget build(BuildContext context) {
+ final textScaler = MediaQuery.textScalerOf(context);
+ final baseStyle = AppTextStyles.bandStatement.copyWith(color: color);
+ final symbolStyle = baseStyle.copyWith(
+ fontSize: CalculatorBand.moneySymbolSize,
+ letterSpacing: -0.4,
+ );
+
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ final symbolWidth = statement.isMoney
+ ? _measure('R\$', symbolStyle, textScaler) + 6
+ : 0.0;
+ final available = constraints.maxWidth - symbolWidth;
+ final naturalWidth = _measure(statement.value, baseStyle, textScaler);
+ final baseSize = baseStyle.fontSize!;
+ final fontSize = naturalWidth <= available
+ ? baseSize
+ : math.max(
+ CalculatorBand.minValueSize,
+ (baseSize * available / naturalWidth).floorToDouble(),
+ );
+
+ return FittedBox(
+ fit: BoxFit.scaleDown,
+ alignment: Alignment.centerLeft,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.baseline,
+ textBaseline: TextBaseline.alphabetic,
+ children: [
+ if (statement.isMoney) ...[
+ Text('R\$', style: symbolStyle),
+ const SizedBox(width: 6),
+ ],
+ Text(
+ statement.value,
+ maxLines: 1,
+ softWrap: false,
+ style: baseStyle.copyWith(fontSize: fontSize),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+
+ static double _measure(String text, TextStyle style, TextScaler scaler) {
+ final painter = TextPainter(
+ text: TextSpan(text: text, style: style),
+ textDirection: TextDirection.ltr,
+ textScaler: scaler,
+ maxLines: 1,
+ )..layout();
+ final width = painter.width;
+ painter.dispose();
+ return width;
+ }
+}
+
+class _BandIconButton extends StatelessWidget {
+ final String icon;
+ final String label;
+ final Color color;
+ final VoidCallback onPressed;
+
+ const _BandIconButton({
+ required this.icon,
+ required this.label,
+ required this.color,
+ required this.onPressed,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return MergeSemantics(
+ child: Semantics(
+ label: label,
+ child: SizedBox.square(
+ dimension: CalculatorBand.iconTouchSize,
+ child: IconButton(
+ onPressed: onPressed,
+ padding: EdgeInsets.zero,
+ color: color,
+ icon: AppIcon(icon, size: 22),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/core/widgets/error_banner.dart b/lib/core/widgets/error_banner.dart
deleted file mode 100644
index 4a41b7d..0000000
--- a/lib/core/widgets/error_banner.dart
+++ /dev/null
@@ -1,63 +0,0 @@
-import 'package:flutter/material.dart';
-
-import '../themes/extensions/error_banner_theme.dart';
-
-class ErrorBanner extends StatelessWidget {
- final String message;
- final VoidCallback? onRetry;
- final String retryLabel;
-
- const ErrorBanner({
- super.key,
- required this.message,
- this.onRetry,
- this.retryLabel = 'Tentar novamente',
- });
-
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context).extension()!;
-
- return Container(
- width: double.infinity,
- padding: theme.padding,
- decoration: BoxDecoration(
- color: theme.backgroundColor,
- borderRadius: BorderRadius.circular(theme.borderRadius),
- border: Border.all(color: theme.borderColor),
- ),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Icon(
- Icons.warning_amber_rounded,
- size: theme.iconSize,
- color: theme.iconColor,
- ),
- SizedBox(width: theme.iconMessageSpacing),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(message, style: theme.messageStyle),
- if (onRetry != null) ...[
- SizedBox(height: theme.retrySpacing),
- TextButton(
- onPressed: onRetry,
- style: TextButton.styleFrom(
- foregroundColor: theme.retryTextColor,
- padding: EdgeInsets.zero,
- minimumSize: Size.zero,
- tapTargetSize: MaterialTapTargetSize.shrinkWrap,
- ),
- child: Text(retryLabel),
- ),
- ],
- ],
- ),
- ),
- ],
- ),
- );
- }
-}
diff --git a/lib/core/widgets/field_list/field_list.dart b/lib/core/widgets/field_list/field_list.dart
new file mode 100644
index 0000000..2d81655
--- /dev/null
+++ b/lib/core/widgets/field_list/field_list.dart
@@ -0,0 +1,26 @@
+import 'package:flutter/material.dart';
+import 'package:real_calc/core/themes/app_colors.dart';
+
+/// Lista de campos: uma coluna com linha `hairline` no topo, sem cartão,
+/// sombra ou ícones. Cada [FieldRow] desenha a própria linha de baixo.
+class FieldList extends StatelessWidget {
+ final List children;
+
+ const FieldList({super.key, required this.children});
+
+ @override
+ Widget build(BuildContext context) {
+ final colors = AppColors.of(context);
+
+ return DecoratedBox(
+ decoration: BoxDecoration(
+ border: Border(top: BorderSide(color: colors.hairline)),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ mainAxisSize: MainAxisSize.min,
+ children: children,
+ ),
+ );
+ }
+}
diff --git a/lib/core/widgets/field_list/field_row.dart b/lib/core/widgets/field_list/field_row.dart
new file mode 100644
index 0000000..0a4ba54
--- /dev/null
+++ b/lib/core/widgets/field_list/field_row.dart
@@ -0,0 +1,381 @@
+import 'package:flutter/material.dart';
+import 'package:real_calc/core/widgets/app_icon.dart';
+import 'package:flutter/services.dart';
+import 'package:real_calc/core/themes/app_colors.dart';
+import 'package:real_calc/core/themes/calculator_palette.dart';
+import 'package:real_calc/core/themes/calculator_scope.dart';
+import 'package:real_calc/core/themes/radius.dart';
+import 'package:real_calc/core/themes/spacing.dart';
+import 'package:real_calc/core/themes/text_styles.dart';
+
+import '../types/input_field_state.dart';
+
+/// Linha de um campo da [FieldList].
+///
+/// À esquerda ficam o rótulo e uma segunda linha opcional; à direita, sem
+/// quebra, o prefixo, o valor e a unidade. A cor de destaque vem do
+/// [CalculatorScope] mais próximo.
+///
+/// Estados:
+/// - neutro;
+/// - em edição (com foco): linha de 2px e cursor na cor da calculadora;
+/// - [InputFieldState.unknown]: "a descobrir" e pílula "Toque para calcular";
+/// - [InputFieldState.calculated]: "calculado para você", ✓ e valor em
+/// destaque;
+/// - [InputFieldState.error]: linha, valor e segunda linha em `error`;
+/// - seleção ([FieldRow.selection]): valor e chevron, abre um seletor.
+class FieldRow extends StatefulWidget {
+ static const unknownText = 'a descobrir';
+ static const calculatedText = 'calculado para você';
+ static const calculateText = 'Toque para calcular';
+
+ final String label;
+
+ /// Segunda linha opcional, como "todo mês" ou "opcional". Nos estados
+ /// a descobrir e calculado é trocada pelo texto do estado.
+ final String? secondLine;
+
+ /// Texto da segunda linha no estado de erro (ex.: "fora do período").
+ final String? errorText;
+
+ final InputFieldState state;
+ final TextEditingController? controller;
+ final FocusNode? focusNode;
+ final String? prefix;
+ final String? unit;
+ final String hint;
+ final TextInputType keyboardType;
+ final List? inputFormatters;
+ final ValueChanged? onChanged;
+
+ /// Chamado ao tocar na linha no estado a descobrir.
+ final VoidCallback? onCalculate;
+
+ /// Espaço mantido entre o campo em foco e o teclado.
+ final EdgeInsets scrollPadding;
+
+ final String? _selectionValue;
+ final VoidCallback? _onSelect;
+
+ const FieldRow({
+ super.key,
+ required this.label,
+ required TextEditingController this.controller,
+ this.secondLine,
+ this.errorText,
+ this.state = InputFieldState.neutral,
+ this.focusNode,
+ this.prefix,
+ this.unit,
+ this.hint = '0',
+ this.keyboardType = const TextInputType.numberWithOptions(decimal: true),
+ this.inputFormatters,
+ this.onChanged,
+ this.onCalculate,
+ this.scrollPadding = const EdgeInsets.fromLTRB(20, 20, 20, 120),
+ }) : _selectionValue = null,
+ _onSelect = null;
+
+ /// Linha de seleção (ex.: Índice): mostra o valor e um chevron e chama
+ /// [onTap] ao ser tocada.
+ const FieldRow.selection({
+ super.key,
+ required this.label,
+ required String value,
+ required VoidCallback onTap,
+ this.secondLine,
+ this.errorText,
+ this.state = InputFieldState.neutral,
+ }) : _selectionValue = value,
+ _onSelect = onTap,
+ controller = null,
+ focusNode = null,
+ prefix = null,
+ unit = null,
+ hint = '',
+ keyboardType = TextInputType.none,
+ inputFormatters = null,
+ onChanged = null,
+ onCalculate = null,
+ scrollPadding = EdgeInsets.zero;
+
+ bool get isSelection => _onSelect != null;
+
+ @override
+ State createState() => _FieldRowState();
+}
+
+class _FieldRowState extends State {
+ /// Parte máxima da linha ocupada pelo bloco da direita; o resto fica para
+ /// o rótulo e a segunda linha, que quebram.
+ static const _maxTrailingFraction = 0.65;
+
+ FocusNode? _ownFocusNode;
+
+ FocusNode get _focusNode =>
+ widget.focusNode ?? (_ownFocusNode ??= FocusNode());
+
+ @override
+ void initState() {
+ super.initState();
+ _focusNode.addListener(_rebuild);
+ widget.controller?.addListener(_rebuild);
+ }
+
+ @override
+ void didUpdateWidget(FieldRow oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.focusNode != widget.focusNode) {
+ (oldWidget.focusNode ?? _ownFocusNode)?.removeListener(_rebuild);
+ _focusNode.addListener(_rebuild);
+ }
+ if (oldWidget.controller != widget.controller) {
+ oldWidget.controller?.removeListener(_rebuild);
+ widget.controller?.addListener(_rebuild);
+ }
+ }
+
+ @override
+ void dispose() {
+ _focusNode.removeListener(_rebuild);
+ widget.controller?.removeListener(_rebuild);
+ _ownFocusNode?.dispose();
+ super.dispose();
+ }
+
+ void _rebuild() => setState(() {});
+
+ bool get _isUnknown => widget.state == InputFieldState.unknown;
+ bool get _isCalculated => widget.state == InputFieldState.calculated;
+ bool get _isError => widget.state == InputFieldState.error;
+ bool get _isEditing =>
+ !widget.isSelection && !_isUnknown && _focusNode.hasFocus;
+
+ void _handleTap() {
+ if (widget.isSelection) {
+ widget._onSelect!();
+ } else if (_isUnknown) {
+ widget.onCalculate?.call();
+ } else {
+ _focusNode.requestFocus();
+ }
+ }
+
+ CalculatorPalette _palette(BuildContext context) {
+ final scoped = CalculatorScope.maybeOf(context);
+ if (scoped != null) return scoped;
+ final scheme = Theme.of(context).colorScheme;
+ return CalculatorPalette(
+ solid: scheme.primary,
+ onSolid: scheme.onPrimary,
+ text: scheme.primary,
+ );
+ }
+
+ String? get _stateText {
+ if (_isError) return widget.errorText ?? widget.secondLine;
+ if (_isUnknown) return FieldRow.unknownText;
+ if (_isCalculated) return FieldRow.calculatedText;
+ return widget.secondLine;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final colors = AppColors.of(context);
+ final palette = _palette(context);
+ final secondText = _stateText;
+
+ final (lineColor, lineWidth) = switch (true) {
+ _ when _isError => (colors.error, 1.5),
+ _ when _isUnknown || _isEditing => (palette.solid, 2.0),
+ _ => (colors.hairline, 1.0),
+ };
+
+ final secondStyle = (_isError || _isUnknown || _isCalculated)
+ ? AppTextStyles.rowState.copyWith(
+ color: _isError ? colors.error : palette.text,
+ )
+ : AppTextStyles.caption.copyWith(color: colors.textSecondary);
+
+ final row = GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: _handleTap,
+ child: Container(
+ constraints: BoxConstraints(
+ minHeight: secondText == null
+ ? AppSpacing.rowHeight
+ : AppSpacing.rowHeightWithSecondLine,
+ ),
+ padding: const EdgeInsets.symmetric(vertical: 10),
+ decoration: BoxDecoration(
+ border: Border(
+ bottom: BorderSide(color: lineColor, width: lineWidth),
+ ),
+ ),
+ child: LayoutBuilder(
+ builder: (context, constraints) => Row(
+ children: [
+ Expanded(
+ child: ExcludeSemantics(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ widget.label,
+ style: AppTextStyles.rowLabel.copyWith(
+ color: colors.textPrimary,
+ ),
+ ),
+ if (secondText != null) ...[
+ const SizedBox(height: 2),
+ Text(secondText, style: secondStyle),
+ ],
+ ],
+ ),
+ ),
+ ),
+ const SizedBox(width: AppSpacing.md),
+ // Valor sem quebra: se não couber (tela estreita e fonte grande),
+ // diminui inteiro em vez de ser cortado.
+ ConstrainedBox(
+ constraints: BoxConstraints(
+ maxWidth: constraints.maxWidth * _maxTrailingFraction,
+ ),
+ child: FittedBox(
+ fit: BoxFit.scaleDown,
+ alignment: Alignment.centerRight,
+ child: _buildTrailing(context, colors, palette),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+
+ if (widget.isSelection || _isUnknown) {
+ final value = widget.isSelection
+ ? widget._selectionValue
+ : FieldRow.unknownText;
+ return Semantics(
+ container: true,
+ button: true,
+ label: [
+ widget.label,
+ value,
+ if (widget.isSelection && secondText != null) secondText,
+ ].join(', '),
+ onTap: _handleTap,
+ excludeSemantics: true,
+ child: row,
+ );
+ }
+
+ // O TextField contribui com o valor; o rótulo junta nome, unidade e
+ // estado, na ordem em que o TalkBack deve ler.
+ return MergeSemantics(
+ child: Semantics(
+ label: [widget.label, ?widget.unit, ?secondText].join(', '),
+ child: row,
+ ),
+ );
+ }
+
+ Widget _buildTrailing(
+ BuildContext context,
+ AppColors colors,
+ CalculatorPalette palette,
+ ) {
+ if (widget.isSelection) {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ widget._selectionValue!,
+ style: AppTextStyles.rowValue.copyWith(
+ color: _isError ? colors.error : colors.textPrimary,
+ ),
+ ),
+ const SizedBox(width: AppSpacing.xs),
+ AppIcon(AppIcons.chevronRight, size: 20, color: colors.textSecondary),
+ ],
+ );
+ }
+
+ if (_isUnknown) {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
+ decoration: BoxDecoration(
+ color: palette.solid,
+ borderRadius: BorderRadius.circular(AppRadius.pill),
+ ),
+ child: Text(
+ FieldRow.calculateText,
+ style: AppTextStyles.pill.copyWith(color: palette.onSolid),
+ ),
+ );
+ }
+
+ final valueStyle = AppTextStyles.rowValue.copyWith(
+ color: switch (widget.state) {
+ InputFieldState.error => colors.error,
+ InputFieldState.calculated => palette.text,
+ _ => colors.textPrimary,
+ },
+ fontWeight: _isCalculated ? FontWeight.w800 : null,
+ );
+ final unitStyle = AppTextStyles.rowUnit.copyWith(
+ color: _isError ? colors.error : colors.textSecondary,
+ );
+
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.baseline,
+ textBaseline: TextBaseline.alphabetic,
+ children: [
+ if (_isCalculated) ...[
+ AppIcon(AppIcons.check, size: 16, color: palette.text),
+ const SizedBox(width: 6),
+ ],
+ if (widget.prefix != null) ...[
+ ExcludeSemantics(child: Text(widget.prefix!, style: unitStyle)),
+ const SizedBox(width: AppSpacing.xs),
+ ],
+ // Largura do conteúdo (ou do hint), para prefixo, valor e unidade
+ // ficarem colados.
+ IntrinsicWidth(
+ child: TextField(
+ controller: widget.controller,
+ focusNode: _focusNode,
+ keyboardType: widget.keyboardType,
+ inputFormatters: widget.inputFormatters,
+ onChanged: widget.onChanged,
+ textAlign: TextAlign.end,
+ textInputAction: TextInputAction.done,
+ style: valueStyle,
+ cursorColor: palette.solid,
+ scrollPadding: widget.scrollPadding,
+ decoration: InputDecoration(
+ hintText: widget.hint,
+ hintStyle: valueStyle.copyWith(color: colors.textHint),
+ isCollapsed: true,
+ filled: false,
+ contentPadding: EdgeInsets.zero,
+ border: InputBorder.none,
+ enabledBorder: InputBorder.none,
+ focusedBorder: InputBorder.none,
+ errorBorder: InputBorder.none,
+ disabledBorder: InputBorder.none,
+ focusedErrorBorder: InputBorder.none,
+ ),
+ ),
+ ),
+ if (widget.unit != null) ...[
+ const SizedBox(width: AppSpacing.xs),
+ ExcludeSemantics(child: Text(widget.unit!, style: unitStyle)),
+ ],
+ ],
+ );
+ }
+}
diff --git a/lib/core/widgets/form_notice.dart b/lib/core/widgets/form_notice.dart
new file mode 100644
index 0000000..842c184
--- /dev/null
+++ b/lib/core/widgets/form_notice.dart
@@ -0,0 +1,58 @@
+import 'package:flutter/material.dart';
+import 'package:real_calc/core/widgets/app_icon.dart';
+import 'package:real_calc/core/themes/app_colors.dart';
+import 'package:real_calc/core/themes/text_styles.dart';
+
+class FormNotice extends StatelessWidget {
+ final String title;
+ final String message;
+ final bool isError;
+
+ const FormNotice({
+ super.key,
+ required this.title,
+ required this.message,
+ this.isError = false,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final colors = AppColors.of(context);
+ final accent = isError ? colors.error : colors.textSecondary;
+
+ return MergeSemantics(
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(top: 1),
+ child: AppIcon(AppIcons.alert, size: 20, color: accent),
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ title,
+ style: AppTextStyles.rowLabel.copyWith(
+ color: isError ? colors.error : colors.textPrimary,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ const SizedBox(height: 2),
+ Text(
+ message,
+ style: AppTextStyles.body.copyWith(
+ color: colors.textSecondary,
+ fontSize: 14,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/core/widgets/help_card.dart b/lib/core/widgets/help_card.dart
deleted file mode 100644
index cef5b63..0000000
--- a/lib/core/widgets/help_card.dart
+++ /dev/null
@@ -1,52 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:real_calc/core/themes/extensions/help_card_theme.dart';
-
-class HelpCard extends StatelessWidget {
- final String message;
-
- const HelpCard({super.key, required this.message});
-
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context).extension()!;
-
- return Container(
- width: double.infinity,
- padding: theme.padding,
- decoration: BoxDecoration(
- color: theme.backgroundColor,
- borderRadius: BorderRadius.circular(theme.borderRadius),
- border: Border.all(
- color: theme.borderColor.withValues(alpha: 0.4),
- ),
- ),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Container(
- height: 40,
- width: 40,
- decoration: BoxDecoration(
- color: theme.iconBackgroundColor,
- borderRadius: BorderRadius.circular(12),
- ),
- child: Icon(
- Icons.lightbulb,
- color: theme.iconColor,
- size: theme.iconSize,
- ),
- ),
- const SizedBox(width: 14),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(message, style: theme.messageStyle),
- ],
- ),
- ),
- ],
- ),
- );
- }
-}
\ No newline at end of file
diff --git a/lib/core/widgets/help_sheet/help_content.dart b/lib/core/widgets/help_sheet/help_content.dart
new file mode 100644
index 0000000..9f00bda
--- /dev/null
+++ b/lib/core/widgets/help_sheet/help_content.dart
@@ -0,0 +1,131 @@
+import 'package:real_calc/core/themes/calculator_palette.dart';
+
+class HelpField {
+ final String name;
+ final String description;
+
+ const HelpField(this.name, this.description);
+}
+
+class HelpContent {
+ final String intro;
+ final List fields;
+ final String example;
+ final String exampleResult;
+ final String methodology;
+ final String formula;
+
+ const HelpContent({
+ required this.intro,
+ required this.fields,
+ required this.example,
+ required this.exampleResult,
+ required this.methodology,
+ required this.formula,
+ });
+
+ static HelpContent of(CalculatorKind kind) => switch (kind) {
+ CalculatorKind.financing => financing,
+ CalculatorKind.deposits => deposits,
+ CalculatorKind.futureValue => futureValue,
+ CalculatorKind.correction => correction,
+ };
+
+ static const _rate = HelpField(
+ 'Taxa de juros',
+ 'Os juros de cada mês, em porcentagem.',
+ );
+
+ static const financing = HelpContent(
+ intro:
+ 'Descubra a prestação, o valor financiado, o prazo ou a taxa. '
+ 'Preencha três campos e o app calcula o que falta.',
+ fields: [
+ HelpField(
+ 'Valor financiado',
+ 'Quanto você vai pegar emprestado, sem os juros.',
+ ),
+ HelpField('Prazo', 'Em quantos meses a dívida será paga.'),
+ _rate,
+ HelpField('Prestação', 'O valor fixo pago todo mês.'),
+ ],
+ example: 'R\$ 2.500,00 a 2% ao mês, em 12 meses, dão uma prestação de',
+ exampleResult: 'R\$ 236,40',
+ methodology:
+ 'Usamos a tabela Price, com prestações iguais. V é o valor '
+ 'financiado, i a taxa mensal e n o prazo.',
+ formula: 'P = V × i ÷ (1 − (1 + i)⁻ⁿ)',
+ );
+
+ static const deposits = HelpContent(
+ intro:
+ 'Veja quanto junta guardando o mesmo valor todo mês, ou descubra o '
+ 'depósito, o prazo ou a taxa para chegar a um objetivo.',
+ fields: [
+ HelpField(
+ 'Depósito mensal',
+ 'O valor guardado todo mês, sempre no início do mês.',
+ ),
+ HelpField('Prazo', 'Por quantos meses você vai depositar.'),
+ _rate,
+ HelpField('Valor final', 'Quanto você terá no fim do prazo.'),
+ ],
+ example: 'R\$ 100,00 por mês a 1% ao mês, durante 12 meses, viram',
+ exampleResult: 'R\$ 1.280,93',
+ methodology:
+ 'O depósito é feito no início de cada mês e já rende no próprio '
+ 'mês. D é o depósito, i a taxa mensal e n o prazo.',
+ formula: 'VF = D × (1 + i) × ((1 + i)ⁿ − 1) ÷ i',
+ );
+
+ static const futureValue = HelpContent(
+ intro:
+ 'Veja quanto um valor aplicado hoje rende com juros compostos, ou '
+ 'descubra o valor aplicado, o prazo ou a taxa.',
+ fields: [
+ HelpField('Valor aplicado', 'O valor aplicado hoje, uma única vez.'),
+ HelpField('Prazo', 'Por quantos meses o dinheiro fica aplicado.'),
+ _rate,
+ HelpField(
+ 'Valor futuro',
+ 'Quanto o valor aplicado vale no fim do prazo.',
+ ),
+ ],
+ example: 'R\$ 1.000,00 a 1% ao mês, durante 12 meses, viram',
+ exampleResult: 'R\$ 1.126,83',
+ methodology:
+ 'Juros compostos: os juros de cada mês também passam a render. VP '
+ 'é o valor aplicado, i a taxa mensal e n o prazo.',
+ formula: 'VF = VP × (1 + i)ⁿ',
+ );
+
+ static const correction = HelpContent(
+ intro:
+ 'Atualize um valor pela inflação ou por outro índice entre duas '
+ 'datas, com os dados oficiais do Banco Central.',
+ fields: [
+ HelpField(
+ 'Índice de correção',
+ 'A série usada na correção, como IPCA, IGP-M ou CDI.',
+ ),
+ HelpField('Data inicial', 'De quando o valor parte.'),
+ HelpField('Data final', 'Até quando o valor é corrigido.'),
+ HelpField(
+ 'Percentual',
+ 'Só no CDI: quanto do CDI aplicar, como 100% ou 110%.',
+ ),
+ HelpField(
+ 'Valor a ser corrigido',
+ 'O valor na data inicial. Sem ele, mostramos só o fator.',
+ ),
+ ],
+ example:
+ 'Se o índice subiu 0,5% em um mês e 0,4% no seguinte, '
+ 'R\$ 1.000,00 viram',
+ exampleResult: 'R\$ 1.009,02',
+ methodology:
+ 'Multiplicamos as variações de cada período e aplicamos o fator ao '
+ 'valor. i₁ … iₙ são as variações do índice no período.',
+ formula: 'fator = (1 + i₁) × … × (1 + iₙ)',
+ );
+}
diff --git a/lib/core/widgets/help_sheet/help_sheet.dart b/lib/core/widgets/help_sheet/help_sheet.dart
new file mode 100644
index 0000000..954d222
--- /dev/null
+++ b/lib/core/widgets/help_sheet/help_sheet.dart
@@ -0,0 +1,221 @@
+import 'package:flutter/material.dart';
+import 'package:real_calc/core/themes/app_colors.dart';
+import 'package:real_calc/core/themes/calculator_palette.dart';
+import 'package:real_calc/core/themes/calculator_scope.dart';
+import 'package:real_calc/core/themes/spacing.dart';
+import 'package:real_calc/core/themes/text_styles.dart';
+
+import '../app_sheet.dart';
+import 'help_content.dart';
+import 'methodology.dart';
+
+const helpSheetTitle = 'Como funciona';
+
+Future showHelpSheet(BuildContext context, CalculatorKind kind) {
+ return showAppSheet(
+ context: context,
+ title: helpSheetTitle,
+ kind: kind,
+ bodyBuilder: (_) => HelpSheetBody(
+ content: HelpContent.of(kind),
+ methodology: Methodology.of(kind),
+ ),
+ footerBuilder: (context) => const _UnderstoodButton(),
+ );
+}
+
+class HelpSheetBody extends StatelessWidget {
+ final HelpContent content;
+ final Methodology? methodology;
+
+ const HelpSheetBody({super.key, required this.content, this.methodology});
+
+ @override
+ Widget build(BuildContext context) {
+ final colors = AppColors.of(context);
+ final palette = CalculatorScope.of(context);
+ final body = AppTextStyles.body.copyWith(color: colors.textPrimary);
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text(content.intro, style: body),
+ _Section(title: 'O que cada campo significa', colors: colors),
+ DecoratedBox(
+ decoration: BoxDecoration(
+ border: Border(top: BorderSide(color: colors.hairline)),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ for (final field in content.fields)
+ _FieldDescription(field: field, colors: colors),
+ ],
+ ),
+ ),
+ _Section(title: 'Exemplo', colors: colors),
+ Text(content.example, style: body),
+ const SizedBox(height: AppSpacing.xs),
+ Text(
+ content.exampleResult,
+ style: AppTextStyles.exampleValue.copyWith(color: palette.text),
+ ),
+ _Section(title: 'Como calculamos', colors: colors),
+ Text(
+ content.methodology,
+ style: AppTextStyles.body.copyWith(color: colors.textSecondary),
+ ),
+ const SizedBox(height: AppSpacing.sm),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
+ decoration: BoxDecoration(
+ color: colors.elevated,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(
+ content.formula,
+ style: AppTextStyles.detailValue.copyWith(
+ color: colors.textPrimary,
+ ),
+ ),
+ ),
+ if (methodology case final methodology?) ...[
+ _Section(title: Methodology.title, colors: colors),
+ DecoratedBox(
+ decoration: BoxDecoration(
+ border: Border(top: BorderSide(color: colors.hairline)),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ for (final item in methodology.items)
+ _MethodologyItem(item: item, colors: colors),
+ ],
+ ),
+ ),
+ ],
+ ],
+ );
+ }
+}
+
+class _MethodologyItem extends StatelessWidget {
+ final MethodologyItem item;
+ final AppColors colors;
+
+ const _MethodologyItem({required this.item, required this.colors});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ decoration: BoxDecoration(
+ border: Border(bottom: BorderSide(color: colors.hairline)),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Semantics(
+ header: true,
+ child: Text(
+ item.title,
+ style: AppTextStyles.rowLabel.copyWith(
+ color: colors.textPrimary,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ ),
+ const SizedBox(height: 2),
+ Text(
+ item.text,
+ style: AppTextStyles.body.copyWith(color: colors.textSecondary),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _Section extends StatelessWidget {
+ final String title;
+ final AppColors colors;
+
+ const _Section({required this.title, required this.colors});
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.only(
+ top: AppSpacing.sectionGap,
+ bottom: AppSpacing.sm,
+ ),
+ child: Semantics(
+ header: true,
+ child: Text(
+ title,
+ style: AppTextStyles.sectionLabel.copyWith(
+ color: colors.textSecondary,
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _FieldDescription extends StatelessWidget {
+ final HelpField field;
+ final AppColors colors;
+
+ const _FieldDescription({required this.field, required this.colors});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ decoration: BoxDecoration(
+ border: Border(bottom: BorderSide(color: colors.hairline)),
+ ),
+ child: MergeSemantics(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ field.name,
+ style: AppTextStyles.rowLabel.copyWith(color: colors.textPrimary),
+ ),
+ const SizedBox(height: 2),
+ Text(
+ field.description,
+ style: AppTextStyles.listDesc.copyWith(
+ color: colors.textSecondary,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _UnderstoodButton extends StatelessWidget {
+ const _UnderstoodButton();
+
+ @override
+ Widget build(BuildContext context) {
+ final palette = CalculatorScope.of(context);
+
+ return SizedBox(
+ height: 56,
+ child: FilledButton(
+ onPressed: () => Navigator.of(context).pop(),
+ style: FilledButton.styleFrom(
+ backgroundColor: palette.solid,
+ foregroundColor: palette.onSolid,
+ shape: const StadiumBorder(),
+ textStyle: AppTextStyles.button,
+ ),
+ child: const Text('Entendi'),
+ ),
+ );
+ }
+}
diff --git a/lib/core/widgets/help_sheet/methodology.dart b/lib/core/widgets/help_sheet/methodology.dart
new file mode 100644
index 0000000..ec90fd5
--- /dev/null
+++ b/lib/core/widgets/help_sheet/methodology.dart
@@ -0,0 +1,57 @@
+import 'package:real_calc/core/themes/calculator_palette.dart';
+
+class MethodologyItem {
+ final String title;
+ final String text;
+
+ const MethodologyItem(this.title, this.text);
+}
+
+/// Notas de metodologia da folha "Como funciona". Fica separado do
+/// [HelpContent] para outras explicações, como SAC e Price, usarem a mesma
+/// estrutura.
+class Methodology {
+ static const title = 'Metodologia';
+
+ final List items;
+
+ const Methodology(this.items);
+
+ static Methodology? of(CalculatorKind kind) => switch (kind) {
+ CalculatorKind.correction => correction,
+ _ => null,
+ };
+
+ static const correction = Methodology([
+ MethodologyItem(
+ 'Fator acumulado',
+ 'O fator junta as variações do índice no período, uma multiplicando a '
+ 'outra. Com 1% em um mês e 2% no seguinte, o fator é 1,0302, e '
+ 'não 1,03.',
+ ),
+ MethodologyItem(
+ 'Mês inicial e mês final',
+ 'Nos índices mensais, os dois meses entram no cálculo. De 01/2024 até '
+ '03/2024 usa as variações de janeiro, fevereiro e março. Nos '
+ 'índices diários, como CDI e Selic, a conta começa no dia seguinte '
+ 'à data inicial.',
+ ),
+ MethodologyItem(
+ 'Um mês só',
+ 'Para corrigir um único mês, use o mesmo mês em De e Até. De 05/2024 '
+ 'até 05/2024 aplica só a variação de maio.',
+ ),
+ MethodologyItem(
+ 'Valores antes do Real',
+ 'Antes de 07/1994, informe o valor na moeda da época, como cruzeiro ou '
+ 'cruzado. O app não converte moedas: o resultado sai na mesma moeda '
+ 'do valor informado.',
+ ),
+ MethodologyItem(
+ 'IGP-M',
+ 'Usamos a série de alta precisão do IGP-M no Banco Central, com dados '
+ 'desde 06/1989. Em períodos que começam antes de 03/2021, o '
+ 'resultado pode diferir em centavos de outras calculadoras.',
+ ),
+ ]);
+}
diff --git a/lib/core/widgets/input_forms.dart b/lib/core/widgets/input_forms.dart
deleted file mode 100644
index 5981f97..0000000
--- a/lib/core/widgets/input_forms.dart
+++ /dev/null
@@ -1,69 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:real_calc/core/themes/color_tokens.dart';
-import 'package:real_calc/core/themes/spacing.dart';
-
-class InputForms extends StatelessWidget {
- final String label;
- final String hint;
- final TextEditingController controller;
- final Widget prefixIcon;
- final Widget? suffixIcon;
- final TextInputType keyboardType;
- final TextInputAction? textInputAction;
- final GestureTapCallback? onTap;
- final String? Function(String?)? validator;
- final List? inputFormatters;
-
- const InputForms({
- super.key,
- required this.label,
- required this.hint,
- required this.controller,
- required this.prefixIcon,
- this.suffixIcon,
- this.onTap,
- this.textInputAction,
- this.keyboardType = TextInputType.number,
- this.validator,
- this.inputFormatters,
- });
-
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context);
-
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- spacing: AppSpacing.sm,
- children: [
- Text(
- label,
- style: theme.textTheme.bodyMedium?.copyWith(
- color: ColorTokens.textSecondary,
- fontWeight: FontWeight.w600,
- ),
- ),
- TextFormField(
- validator: validator,
- controller: controller,
- inputFormatters: inputFormatters,
- keyboardType: keyboardType,
- textInputAction: textInputAction,
- onTap: onTap,
- style: theme.textTheme.bodyLarge?.copyWith(
- color: ColorTokens.textPrimary,
- ),
- decoration: InputDecoration(
- hintText: hint,
- hintStyle:
- theme.inputDecorationTheme.hintStyle ??
- TextStyle(color: ColorTokens.textHint),
- prefixIcon: prefixIcon,
- suffixIcon: suffixIcon,
- ),
- ),
- ],
- );
- }
-}
diff --git a/lib/core/widgets/input_forms_result_card.dart b/lib/core/widgets/input_forms_result_card.dart
deleted file mode 100644
index b8792e6..0000000
--- a/lib/core/widgets/input_forms_result_card.dart
+++ /dev/null
@@ -1,171 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:real_calc/core/themes/extensions/input_forms_result_card_theme.dart';
-
-import 'types/input_field_state.dart';
-
-class InputFormsResultCard extends StatelessWidget {
- final String label;
- final String hint;
- final IconData icon;
- final TextEditingController controller;
- final String? Function(String?)? validator;
- final List? inputFormatters;
- final InputFieldState state;
- final VoidCallback? onTap;
- final bool readOnly;
- final String? trailingBadge;
-
- const InputFormsResultCard({
- super.key,
- required this.controller,
- required this.label,
- required this.hint,
- required this.icon,
- this.validator,
- this.inputFormatters,
- this.state = InputFieldState.neutral,
- this.onTap,
- this.readOnly = false,
- this.trailingBadge,
- });
- bool get _isHighlightedAndEmptyText {
- if (state == InputFieldState.highlighted && controller.text.isEmpty) {
- return true;
- }
- return false;
- }
-
- Color _accentColor(InputFormsResultCardTheme theme) {
- return switch (state) {
- InputFieldState.neutral => theme.neutralIconColor,
- InputFieldState.highlighted => theme.highlightedColor,
- InputFieldState.calculated => theme.calculatedColor,
- InputFieldState.error => theme.errorColor,
- };
- }
-
- String? get _badgeText => switch (state) {
- InputFieldState.highlighted => 'vazio',
- InputFieldState.calculated => 'calculado',
- InputFieldState.neutral || InputFieldState.error => null,
- };
-
- String? get _resolvedBadgeText => trailingBadge ?? _badgeText;
-
- String get _effectiveHint {
- if (_isHighlightedAndEmptyText) {
- return 'toque para calcular';
- }
- return hint;
- }
-
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context).extension()!;
- final bool isNeutral = state == InputFieldState.neutral;
- final Color accent = _accentColor(theme);
- final String? badgeText = _resolvedBadgeText;
- final String? errorText = validator?.call(controller.text);
-
- final bool isHintMessage = _isHighlightedAndEmptyText == true;
-
- return Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
- decoration: BoxDecoration(
- color: isNeutral ? null : accent.withValues(alpha: 0.06),
- borderRadius: BorderRadius.circular(theme.borderRadius),
- border: Border.all(
- color: isNeutral ? theme.neutralBorderColor : accent,
- width: isNeutral ? 0.5 : 1,
- ),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- label,
- style: TextStyle(
- fontSize: 12,
- color: isNeutral ? theme.neutralLabelColor : accent,
- ),
- ),
- if (badgeText != null)
- Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 8,
- vertical: 2,
- ),
- decoration: BoxDecoration(
- color: accent.withValues(alpha: 0.15),
- borderRadius: BorderRadius.circular(6),
- ),
- child: Text(
- badgeText,
- style: theme.badgeStyle.copyWith(color: accent),
- ),
- ),
- ],
- ),
- const SizedBox(height: 6),
- Row(
- children: [
- Icon(
- icon,
- size: 18,
- color: isNeutral ? theme.neutralIconColor : accent,
- ),
- const SizedBox(width: 12),
- Expanded(
- child: TextFormField(
- controller: controller,
- onTap: onTap,
- readOnly: readOnly,
- validator: validator,
- inputFormatters: inputFormatters,
- keyboardType: const TextInputType.numberWithOptions(
- decimal: true,
- ),
- textInputAction: TextInputAction.done,
- enableInteractiveSelection: !readOnly,
- showCursor: !readOnly,
- style: const TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.w500,
- ),
- decoration: InputDecoration(
- hintText: _effectiveHint,
- hintStyle: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.w500,
- color: isHintMessage
- ? accent // cor de destaque quando é mensagem
- : theme.neutralLabelColor, // cor normal do hint
- ),
- errorText: errorText,
- errorStyle: theme.badgeStyle.copyWith(
- color: theme.errorColor,
- ),
- isDense: true,
- contentPadding: EdgeInsets.zero,
- filled: false,
- border: InputBorder.none,
- enabledBorder: InputBorder.none,
- focusedBorder: InputBorder.none,
- errorBorder: InputBorder.none,
- disabledBorder: InputBorder.none,
- focusedErrorBorder: InputBorder.none,
- ),
- ),
- ),
- ],
- ),
- ],
- ),
- );
- }
-}
diff --git a/lib/core/widgets/options_bottom_forms.dart b/lib/core/widgets/options_bottom_forms.dart
deleted file mode 100644
index 69e7ef4..0000000
--- a/lib/core/widgets/options_bottom_forms.dart
+++ /dev/null
@@ -1,78 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:real_calc/core/themes/extensions/options_bottom_forms_theme.dart';
-import 'package:real_calc/core/themes/spacing.dart';
-
-class OptionsBottomForms extends StatelessWidget {
- final VoidCallback? onCalculate;
- final VoidCallback? onClear;
- final String calculateLabel;
- final String clearLabel;
-
- const OptionsBottomForms({
- super.key,
- this.onCalculate,
- this.onClear,
- this.calculateLabel = 'Calcular',
- this.clearLabel = 'Limpar',
- });
-
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context);
- final optionsTheme = theme.extension()!;
-
- return Row(
- mainAxisSize: MainAxisSize.min,
- spacing: AppSpacing.sm + 4,
- children: [
- Expanded(
- child: SizedBox(
- width: double.infinity,
- child: ElevatedButton(
- onPressed: onCalculate,
- style: ElevatedButton.styleFrom(
- backgroundColor: optionsTheme.calculateButtonBackground,
- foregroundColor: optionsTheme.calculateButtonForeground,
- padding: const EdgeInsets.symmetric(vertical: AppSpacing.md),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
- ),
- ),
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: Text(
- calculateLabel,
- style: theme.textTheme.labelLarge?.copyWith(
- fontWeight: FontWeight.w600,
- color: Colors.black,
- ),
- ),
- ),
- ),
- ),
- ),
- Expanded(
- child: ElevatedButton(
- onPressed: onClear,
- style: ElevatedButton.styleFrom(
- backgroundColor: optionsTheme.actionButtonBackground,
- foregroundColor: optionsTheme.actionButtonForeground,
- padding: const EdgeInsets.symmetric(vertical: AppSpacing.md),
- textStyle: theme.textTheme.labelLarge?.copyWith(
- fontWeight: FontWeight.w500,
- ),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
- side: BorderSide(color: optionsTheme.borderButtonColor),
- ),
- ),
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: Text(clearLabel),
- ),
- ),
- ),
- ],
- );
- }
-}
diff --git a/lib/core/widgets/page_header.dart b/lib/core/widgets/page_header.dart
deleted file mode 100644
index 62374d6..0000000
--- a/lib/core/widgets/page_header.dart
+++ /dev/null
@@ -1,44 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:real_calc/core/themes/extensions/page_header_theme.dart';
-
-class PageHeader extends StatelessWidget implements PreferredSizeWidget {
- final String title;
- final VoidCallback? onBack;
-
- const PageHeader({super.key, required this.title, this.onBack});
-
- @override
- Size get preferredSize => const Size.fromHeight(56);
-
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context).extension()!;
-
- return SafeArea(
- bottom: false,
- child: SizedBox(
- height: preferredSize.height,
- child: Container(
- color: theme.backgroundColor,
- padding: const EdgeInsets.symmetric(horizontal: 8),
- child: Row(
- children: [
- IconButton(
- icon: Icon(Icons.arrow_back, color: theme.iconColor, size: 20),
- onPressed: onBack ?? () => Navigator.of(context).maybePop(),
- ),
- const SizedBox(width: 4),
- Expanded(
- child: Text(
- title,
- style: theme.titleStyle,
- overflow: TextOverflow.ellipsis,
- ),
- ),
- ],
- ),
- ),
- ),
- );
- }
-}
\ No newline at end of file
diff --git a/lib/core/widgets/result_details/result_details.dart b/lib/core/widgets/result_details/result_details.dart
new file mode 100644
index 0000000..7cac1a5
--- /dev/null
+++ b/lib/core/widgets/result_details/result_details.dart
@@ -0,0 +1,192 @@
+import 'package:flutter/material.dart';
+import 'package:real_calc/core/themes/app_colors.dart';
+import 'package:real_calc/core/themes/calculator_scope.dart';
+import 'package:real_calc/core/themes/spacing.dart';
+import 'package:real_calc/core/themes/text_styles.dart';
+
+import 'result_summary.dart';
+
+/// Detalhes do resultado, abaixo da lista de campos: título, barra de
+/// composição com legenda e linhas de detalhe. Sem cartão, sombra ou borda.
+///
+/// A cor de juros, rendimento ou correção vem do [CalculatorScope].
+class ResultDetails extends StatelessWidget {
+ static const double barHeight = 6;
+ static const double lineHeight = 32;
+
+ final ResultSummary summary;
+
+ const ResultDetails({super.key, required this.summary});
+
+ @override
+ Widget build(BuildContext context) {
+ final colors = AppColors.of(context);
+ final otherColor =
+ CalculatorScope.maybeOf(context)?.solid ??
+ Theme.of(context).colorScheme.primary;
+ final composition = summary.composition;
+
+ return Padding(
+ padding: const EdgeInsets.only(top: AppSpacing.sectionGap),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ summary.title,
+ style: AppTextStyles.sectionLabel.copyWith(
+ color: colors.textSecondary,
+ ),
+ ),
+ if (composition != null) ...[
+ const SizedBox(height: 12),
+ _CompositionBar(
+ composition: composition,
+ ownColor: colors.textPrimary,
+ otherColor: otherColor,
+ trackColor: colors.hairline,
+ ),
+ const SizedBox(height: 10),
+ ExcludeSemantics(
+ child: Wrap(
+ spacing: AppSpacing.md,
+ runSpacing: AppSpacing.xs,
+ children: [
+ _LegendItem(
+ segment: composition.own,
+ color: colors.textPrimary,
+ textColor: colors.textSecondary,
+ ),
+ _LegendItem(
+ segment: composition.other,
+ color: otherColor,
+ textColor: colors.textSecondary,
+ ),
+ ],
+ ),
+ ),
+ ],
+ const SizedBox(height: AppSpacing.sm),
+ for (final line in summary.lines)
+ _DetailRow(line: line, colors: colors),
+ ],
+ ),
+ );
+ }
+}
+
+class _CompositionBar extends StatelessWidget {
+ final ResultComposition composition;
+ final Color ownColor;
+ final Color otherColor;
+ final Color trackColor;
+
+ const _CompositionBar({
+ required this.composition,
+ required this.ownColor,
+ required this.otherColor,
+ required this.trackColor,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final own = composition.own.percent;
+ final other = composition.other.percent;
+
+ return Semantics(
+ container: true,
+ label: composition.semanticsLabel,
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(ResultDetails.barHeight / 2),
+ child: SizedBox(
+ height: ResultDetails.barHeight,
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ if (own > 0)
+ Expanded(
+ flex: own,
+ child: ColoredBox(color: ownColor),
+ ),
+ if (own > 0 && other > 0) const SizedBox(width: 2),
+ if (other > 0)
+ Expanded(
+ flex: other,
+ child: ColoredBox(color: otherColor),
+ ),
+ if (own == 0 && other == 0)
+ Expanded(child: ColoredBox(color: trackColor)),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _LegendItem extends StatelessWidget {
+ final ResultSegment segment;
+ final Color color;
+ final Color textColor;
+
+ const _LegendItem({
+ required this.segment,
+ required this.color,
+ required this.textColor,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 8,
+ height: 8,
+ decoration: BoxDecoration(color: color, shape: BoxShape.circle),
+ ),
+ const SizedBox(width: 6),
+ Text(
+ '${segment.label} ${segment.percent}%',
+ style: AppTextStyles.caption.copyWith(color: textColor),
+ ),
+ ],
+ );
+ }
+}
+
+class _DetailRow extends StatelessWidget {
+ final ResultDetailLine line;
+ final AppColors colors;
+
+ const _DetailRow({required this.line, required this.colors});
+
+ @override
+ Widget build(BuildContext context) {
+ return MergeSemantics(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(minHeight: ResultDetails.lineHeight),
+ child: Row(
+ children: [
+ Expanded(
+ child: Text(
+ line.label,
+ style: AppTextStyles.detailLabel.copyWith(
+ color: colors.textSecondary,
+ ),
+ ),
+ ),
+ const SizedBox(width: AppSpacing.md),
+ Text(
+ line.value,
+ softWrap: false,
+ style: AppTextStyles.detailValue.copyWith(
+ color: colors.textPrimary,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/core/widgets/result_details/result_summary.dart b/lib/core/widgets/result_details/result_summary.dart
new file mode 100644
index 0000000..5e43e4f
--- /dev/null
+++ b/lib/core/widgets/result_details/result_summary.dart
@@ -0,0 +1,81 @@
+/// Um lado da barra de composição: rótulo e percentual inteiro.
+class ResultSegment {
+ final String label;
+ final int percent;
+
+ const ResultSegment(this.label, this.percent);
+
+ @override
+ bool operator ==(Object other) =>
+ other is ResultSegment &&
+ other.label == label &&
+ other.percent == percent;
+
+ @override
+ int get hashCode => Object.hash(label, percent);
+
+ @override
+ String toString() => '$label $percent%';
+}
+
+/// Linha de detalhe: rótulo à esquerda, valor já formatado à direita.
+class ResultDetailLine {
+ final String label;
+ final String value;
+
+ const ResultDetailLine(this.label, this.value);
+
+ @override
+ bool operator ==(Object other) =>
+ other is ResultDetailLine && other.label == label && other.value == value;
+
+ @override
+ int get hashCode => Object.hash(label, value);
+
+ @override
+ String toString() => '$label: $value';
+}
+
+/// Composição do resultado: o que é da pessoa ([own]) e o que é juros,
+/// rendimento ou correção ([other]). Os percentuais somam 100.
+class ResultComposition {
+ final ResultSegment own;
+ final ResultSegment other;
+
+ const ResultComposition._(this.own, this.other);
+
+ /// Calcula os percentuais a partir dos valores em centavos. O lado [own] é
+ /// arredondado e o [other] fica com o restante, para a soma dar 100.
+ /// Devolve `null` se o total não for positivo ou se alguma parte for
+ /// negativa (ex.: deflação), casos em que a barra não faz sentido.
+ static ResultComposition? fromCents({
+ required String ownLabel,
+ required int ownCents,
+ required String otherLabel,
+ required int otherCents,
+ }) {
+ final total = ownCents + otherCents;
+ if (total <= 0 || ownCents < 0 || otherCents < 0) return null;
+ final ownPercent = (ownCents * 100 / total).round();
+ return ResultComposition._(
+ ResultSegment(ownLabel, ownPercent),
+ ResultSegment(otherLabel, 100 - ownPercent),
+ );
+ }
+
+ /// Texto lido pelo TalkBack, ex.: "Emprestado 88%, Juros 12%".
+ String get semanticsLabel => '$own, $other';
+}
+
+/// Tudo o que o [ResultDetails] mostra abaixo da lista de campos.
+class ResultSummary {
+ final String title;
+ final ResultComposition? composition;
+ final List lines;
+
+ const ResultSummary({
+ required this.title,
+ required this.composition,
+ required this.lines,
+ });
+}
diff --git a/lib/core/widgets/theme_toggle_button.dart b/lib/core/widgets/theme_toggle_button.dart
new file mode 100644
index 0000000..dd3a131
--- /dev/null
+++ b/lib/core/widgets/theme_toggle_button.dart
@@ -0,0 +1,40 @@
+import 'package:flutter/material.dart';
+import 'package:real_calc/core/widgets/app_icon.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:real_calc/core/themes/theme_cubit.dart';
+
+/// Botão que alterna entre o tema claro e o escuro.
+///
+/// Mostra a lua no tema claro e o sol no escuro, sem fundo. A cor padrão é
+/// branca, pensada para a faixa colorida da tela inicial (#119).
+class ThemeToggleButton extends StatelessWidget {
+ static const double touchSize = 44;
+
+ final Color color;
+
+ const ThemeToggleButton({super.key, this.color = Colors.white});
+
+ @override
+ Widget build(BuildContext context) {
+ final brightness = Theme.of(context).brightness;
+ final isDark = brightness == Brightness.dark;
+ final label = isDark
+ ? 'Mudar para o modo claro'
+ : 'Mudar para o modo escuro';
+
+ return MergeSemantics(
+ child: Semantics(
+ label: label,
+ child: SizedBox.square(
+ dimension: touchSize,
+ child: IconButton(
+ onPressed: () => context.read().toggle(brightness),
+ padding: EdgeInsets.zero,
+ color: color,
+ icon: AppIcon(isDark ? AppIcons.sun : AppIcons.moon, size: 22),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/core/widgets/title_widget.dart b/lib/core/widgets/title_widget.dart
deleted file mode 100644
index b7df79e..0000000
--- a/lib/core/widgets/title_widget.dart
+++ /dev/null
@@ -1,36 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:real_calc/core/themes/extensions/title_widget_theme.dart';
-import 'package:real_calc/core/themes/spacing.dart';
-
-class TitleWidget extends StatelessWidget {
- final String title;
- final String subtitle;
- final bool? invert;
-
- const TitleWidget({
- super.key,
- required this.title,
- required this.subtitle,
- this.invert = false,
- });
-
- @override
- Widget build(BuildContext context) {
- final styles = Theme.of(context).extension()!;
-
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- spacing: AppSpacing.sm,
- children: [
- Text(
- title,
- style: invert == false ? styles.titleStyle : styles.subtitleStyle,
- ),
- Text(
- subtitle,
- style: invert == false ? styles.subtitleStyle : styles.titleStyle,
- ),
- ],
- );
- }
-}
diff --git a/lib/core/widgets/types/input_field_state.dart b/lib/core/widgets/types/input_field_state.dart
index aa2d812..5f46ffe 100644
--- a/lib/core/widgets/types/input_field_state.dart
+++ b/lib/core/widgets/types/input_field_state.dart
@@ -1 +1 @@
-enum InputFieldState { neutral, highlighted, calculated, error }
\ No newline at end of file
+enum InputFieldState { neutral, unknown, calculated, error }
\ No newline at end of file
diff --git a/lib/core/widgets/warning_banner.dart b/lib/core/widgets/warning_banner.dart
deleted file mode 100644
index 9d331e3..0000000
--- a/lib/core/widgets/warning_banner.dart
+++ /dev/null
@@ -1,36 +0,0 @@
-import 'package:flutter/material.dart';
-
-import '../themes/extensions/warning_banner_theme.dart';
-
-class WarningBanner extends StatelessWidget {
- final String message;
-
- const WarningBanner({super.key, required this.message});
-
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context).extension()!;
-
- return Container(
- width: double.infinity,
- padding: theme.padding,
- decoration: BoxDecoration(
- color: theme.backgroundColor,
- borderRadius: BorderRadius.circular(theme.borderRadius),
- border: Border.all(color: theme.borderColor),
- ),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Icon(
- Icons.warning_amber_rounded,
- color: theme.iconColor,
- size: theme.iconSize,
- ),
- SizedBox(width: theme.iconTextSpacing),
- Expanded(child: Text(message, style: theme.textStyle)),
- ],
- ),
- );
- }
-}
diff --git a/lib/main.dart b/lib/main.dart
index acc26b9..5ed19b3 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,17 +1,34 @@
import 'package:flutter/material.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/date_symbol_data_local.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+import 'core/themes/theme_cubit.dart';
import 'modules/app_module.dart';
import 'modules/app_widget.dart';
-void main() {
- initializeDateFormatting('pt_BR', null);
+Future main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+ LicenseRegistry.addLicense(() async* {
+ yield LicenseEntryWithLineBreaks([
+ 'Manrope',
+ ], await rootBundle.loadString('assets/fonts/Manrope-OFL.txt'));
+ yield LicenseEntryWithLineBreaks([
+ 'Bricolage Grotesque',
+ ], await rootBundle.loadString('assets/fonts/Bricolage-Grotesque-OFL.txt'));
+ });
+ await initializeDateFormatting('pt_BR', null);
+ // Lido antes do primeiro frame para o app já abrir no tema escolhido.
+ final preferences = await SharedPreferences.getInstance();
runApp(
- ModularApp(
- module: AppModule(),
- child: const AppWidget(),
+ BlocProvider(
+ create: (_) => ThemeCubit(preferences),
+ child: ModularApp(module: AppModule(), child: const AppWidget()),
),
);
}
diff --git a/lib/modules/app_widget.dart b/lib/modules/app_widget.dart
index b5b86c8..0b82b8a 100644
--- a/lib/modules/app_widget.dart
+++ b/lib/modules/app_widget.dart
@@ -1,29 +1,41 @@
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_modular/flutter_modular.dart';
+import '../core/themes/app_colors.dart';
import '../core/themes/app_theme.dart';
+import '../core/themes/theme_cubit.dart';
class AppWidget extends StatelessWidget {
const AppWidget({super.key});
@override
Widget build(BuildContext context) {
- return MaterialApp.router(
- theme: AppTheme.darkTheme,
- darkTheme: AppTheme.lightTheme,
- themeMode: ThemeMode.dark,
- routeInformationParser: Modular.routeInformationParser,
- routerDelegate: Modular.routerDelegate,
- localizationsDelegates: const [
- GlobalMaterialLocalizations.delegate,
- GlobalWidgetsLocalizations.delegate,
- GlobalCupertinoLocalizations.delegate,
- ],
- supportedLocales: const [
- Locale('pt', 'BR'),
- ],
- locale: const Locale('pt', 'BR'),
+ return BlocBuilder(
+ builder: (context, themeMode) => MaterialApp.router(
+ theme: AppTheme.lightTheme,
+ darkTheme: AppTheme.darkTheme,
+ themeMode: themeMode,
+ builder: (context, child) {
+ final colors =
+ Theme.of(context).extension() ?? AppColors.dark;
+ return AnnotatedRegion(
+ value: AppTheme.systemUiOverlayStyle(colors),
+ child: child ?? const SizedBox.shrink(),
+ );
+ },
+ routeInformationParser: Modular.routeInformationParser,
+ routerDelegate: Modular.routerDelegate,
+ localizationsDelegates: const [
+ GlobalMaterialLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ ],
+ supportedLocales: const [Locale('pt', 'BR')],
+ locale: const Locale('pt', 'BR'),
+ ),
);
}
}
diff --git a/lib/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit.dart b/lib/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit.dart
index ed1498f..1798533 100644
--- a/lib/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit.dart
+++ b/lib/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit.dart
@@ -86,7 +86,7 @@ class FinancingFormCubit extends Cubit {
return InputFieldState.neutral;
}
- return InputFieldState.highlighted;
+ return InputFieldState.unknown;
}
void _setFormattedValue(
@@ -118,7 +118,7 @@ class FinancingFormCubit extends Cubit {
_suppressRecompute = true;
_setFormattedValue(initialValue, result.initialValue);
months.text = result.periods > 0 ? result.periods.toString() : '';
- _setFormattedValue(rate, result.rate, fractionDigits: 4);
+ _setFormattedValue(rate, result.rate);
_setFormattedValue(finalValue, result.finalValue);
_suppressRecompute = false;
diff --git a/lib/modules/financial_calculators/presentation/models/field_spec.dart b/lib/modules/financial_calculators/presentation/models/field_spec.dart
deleted file mode 100644
index b90a136..0000000
--- a/lib/modules/financial_calculators/presentation/models/field_spec.dart
+++ /dev/null
@@ -1,18 +0,0 @@
-import 'package:flutter/services.dart';
-import 'package:flutter/material.dart';
-
-class FieldSpec {
- final T field;
- final String label;
- final String hint;
- final IconData icon;
- final List formatters;
-
- const FieldSpec({
- required this.field,
- required this.label,
- required this.hint,
- required this.icon,
- required this.formatters,
- });
-}
diff --git a/lib/modules/financial_calculators/presentation/pages/financing_page.dart b/lib/modules/financial_calculators/presentation/pages/financing_page.dart
index e4bd7d6..360b60a 100644
--- a/lib/modules/financial_calculators/presentation/pages/financing_page.dart
+++ b/lib/modules/financial_calculators/presentation/pages/financing_page.dart
@@ -1,122 +1,91 @@
import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/services.dart';
-import 'package:real_calc/core/themes/spacing.dart';
-import 'package:real_calc/core/widgets/page_header.dart';
-import 'package:real_calc/core/widgets/title_widget.dart';
-import 'package:real_calc/modules/financial_calculators/presentation/models/field_spec.dart';
-import 'package:real_calc/modules/financial_calculators/presentation/widgets/forms_view.dart';
-import 'package:real_calc/core/widgets/help_card.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:real_calc/core/services/result_sharer.dart';
+import 'package:real_calc/core/services/share_plus_result_sharer.dart';
+import 'package:real_calc/core/themes/calculator_palette.dart';
+import 'package:real_calc/core/themes/calculator_scope.dart';
+import 'package:real_calc/core/utils/decimal_input_formatter.dart';
-import '../../../../core/utils/decimal_input_formatter.dart';
import '../../domain/enum/financial_calculation_target.dart';
-import '../../../../core/widgets/types/input_field_state.dart';
import '../cubit/financing/financing_cubit.dart';
import '../cubit/forms/financing_form_cubit.dart';
+import '../utils/financing_statement.dart';
+import '../utils/result_summaries.dart';
+import '../utils/share_texts.dart';
+import '../widgets/calculator_form_view.dart';
class FinancingPage extends StatelessWidget {
- static final List> _fieldSpecs = [
- FieldSpec(
- field: FinancialCalculationTarget.initialValue,
- label: 'Valor financiado (R\$)',
+ static const title = 'Financiamento';
+
+ static final fields = [
+ CalculatorFieldConfig(
+ target: FinancialCalculationTarget.initialValue,
+ label: 'Valor financiado',
+ prefix: 'R\$',
hint: '0,00',
- icon: Icons.attach_money,
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
formatters: [DecimalInputFormatter()],
),
- FieldSpec(
- field: FinancialCalculationTarget.periods,
- label: 'Prazo (meses)',
+ CalculatorFieldConfig(
+ target: FinancialCalculationTarget.periods,
+ label: 'Prazo',
+ unit: 'meses',
hint: '0',
- icon: Icons.calendar_today,
+ keyboardType: TextInputType.number,
formatters: [FilteringTextInputFormatter.digitsOnly],
),
- FieldSpec(
- field: FinancialCalculationTarget.interestRate,
- label: 'Taxa de juros (% ao mês)',
+ CalculatorFieldConfig(
+ target: FinancialCalculationTarget.interestRate,
+ label: 'Taxa de juros',
+ unit: '% a.m.',
hint: '0,00',
- icon: Icons.percent,
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
formatters: [DecimalInputFormatter()],
),
- FieldSpec(
- field: FinancialCalculationTarget.finalValue,
- label: 'Valor da prestação',
+ CalculatorFieldConfig(
+ target: FinancialCalculationTarget.finalValue,
+ label: 'Prestação',
+ prefix: 'R\$',
+ secondLine: 'todo mês',
hint: '0,00',
- icon: Icons.payments,
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
formatters: [DecimalInputFormatter()],
),
];
- const FinancingPage({super.key});
-
- BlocListener _buildForms() {
- return BlocListener(
- listener: (context, state) {
- final formCubit = context.read();
- if (state is FinancingCalculated) {
- formCubit.applyResult(state.value, state.calculatedField);
- } else if (state is FinancingError) {
- formCubit.invalidateCalculatedField();
- }
- },
- child: BlocBuilder(
- builder: (context, financingState) {
- final formCubit = context.watch();
- final formState = formCubit.state;
-
- void calculate() {
- if (formState.canCalculate) {
- context.read().calculate(formCubit.financing);
- }
- }
+ final ResultSharer sharer;
- return FormsView(
- fieldSpecs: _fieldSpecs,
- controllerFor: formCubit.controllerFor,
- stateOf: formState.stateOf,
- isLoading: financingState is FinancingLoading,
- errorMessage: financingState is FinancingError
- ? financingState.message ?? 'Erro ao calcular.'
- : null,
- onCalculate: calculate,
- onClear: formCubit.clear,
- onFieldTap: (field) {
- if (formState.stateOf(field) == InputFieldState.highlighted) {
- calculate();
- }
- },
- );
- },
- ),
- );
- }
+ const FinancingPage({super.key, this.sharer = const SharePlusResultSharer()});
@override
Widget build(BuildContext context) {
- return Scaffold(
- appBar: const PageHeader(title: 'Financiamento'),
- body: SafeArea(
- child: SingleChildScrollView(
- padding: const EdgeInsets.symmetric(
- horizontal: AppSpacing.md + 4,
- vertical: AppSpacing.md + 4,
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- spacing: AppSpacing.lg,
- children: [
- const TitleWidget(
- title: 'Financiamento',
- subtitle: 'Calcule o valor da prestação do seu financiamento.',
- ),
- const HelpCard(
- message:
- 'Preencha 3 campos e toque no 4° para calcular automaticamente',
- ),
- _buildForms(),
- ],
- ),
+ return CalculatorScope(
+ kind: CalculatorKind.financing,
+ child: BlocConsumer(
+ listener: (context, state) {
+ final formCubit = context.read();
+ if (state is FinancingCalculated) {
+ formCubit.applyResult(state.value, state.calculatedField);
+ } else if (state is FinancingError) {
+ formCubit.invalidateCalculatedField();
+ }
+ },
+ builder: (context, state) => CalculatorFormView(
+ kind: CalculatorKind.financing,
+ title: title,
+ fields: fields,
+ statement: financingStatement,
+ missingMessage: financingMissingFieldsMessage,
+ summary: financingResultSummary,
+ onCalculate: context.read().calculate,
+ shareText: financingShareText,
+ sharer: sharer,
+ errorMessage: state is FinancingError
+ ? state.message ?? 'Confira os valores.'
+ : null,
),
),
);
}
-}
\ No newline at end of file
+}
diff --git a/lib/modules/financial_calculators/presentation/pages/future_value_page.dart b/lib/modules/financial_calculators/presentation/pages/future_value_page.dart
index cae86aa..d1803e0 100644
--- a/lib/modules/financial_calculators/presentation/pages/future_value_page.dart
+++ b/lib/modules/financial_calculators/presentation/pages/future_value_page.dart
@@ -1,120 +1,91 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:real_calc/core/themes/spacing.dart';
-import 'package:real_calc/core/widgets/page_header.dart';
-import 'package:real_calc/core/widgets/title_widget.dart';
-import 'package:real_calc/modules/financial_calculators/presentation/models/field_spec.dart';
-import 'package:real_calc/core/widgets/help_card.dart';
+import 'package:real_calc/core/services/result_sharer.dart';
+import 'package:real_calc/core/services/share_plus_result_sharer.dart';
+import 'package:real_calc/core/themes/calculator_palette.dart';
+import 'package:real_calc/core/themes/calculator_scope.dart';
+import 'package:real_calc/core/utils/decimal_input_formatter.dart';
-import '../../../../core/utils/decimal_input_formatter.dart';
import '../../domain/enum/financial_calculation_target.dart';
-import '../../../../core/widgets/types/input_field_state.dart';
import '../cubit/forms/financing_form_cubit.dart';
import '../cubit/future_value/future_value_cubit.dart';
-import '../widgets/forms_view.dart';
+import '../utils/future_value_statement.dart';
+import '../utils/result_summaries.dart';
+import '../utils/share_texts.dart';
+import '../widgets/calculator_form_view.dart';
class FutureValuePage extends StatelessWidget {
- static final List> _fieldSpecs = [
- FieldSpec(
- field: FinancialCalculationTarget.initialValue,
- label: 'Capital (R\$)',
+ static const title = 'Valor Futuro';
+
+ static final fields = [
+ CalculatorFieldConfig(
+ target: FinancialCalculationTarget.initialValue,
+ label: 'Valor aplicado',
+ prefix: 'R\$',
hint: '0,00',
- icon: Icons.attach_money,
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
formatters: [DecimalInputFormatter()],
),
- FieldSpec(
- field: FinancialCalculationTarget.periods,
- label: 'Prazo (meses)',
+ CalculatorFieldConfig(
+ target: FinancialCalculationTarget.periods,
+ label: 'Prazo',
+ unit: 'meses',
hint: '0',
- icon: Icons.calendar_today,
+ keyboardType: TextInputType.number,
formatters: [FilteringTextInputFormatter.digitsOnly],
),
- FieldSpec(
- field: FinancialCalculationTarget.interestRate,
- label: 'Taxa de juros (% ao mês)',
+ CalculatorFieldConfig(
+ target: FinancialCalculationTarget.interestRate,
+ label: 'Taxa de juros',
+ unit: '% a.m.',
hint: '0,00',
- icon: Icons.percent,
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
formatters: [DecimalInputFormatter()],
),
- FieldSpec(
- field: FinancialCalculationTarget.finalValue,
- label: 'Valor final',
+ CalculatorFieldConfig(
+ target: FinancialCalculationTarget.finalValue,
+ label: 'Valor futuro',
+ prefix: 'R\$',
hint: '0,00',
- icon: Icons.payments,
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
formatters: [DecimalInputFormatter()],
),
];
- const FutureValuePage({super.key});
-
- BlocListener _buildForms() {
- return BlocListener(
- listener: (context, state) {
- final formCubit = context.read();
- if (state is FutureValueCalculated) {
- formCubit.applyResult(state.value, state.calculatedField);
- } else if (state is FutureValueError) {
- formCubit.invalidateCalculatedField();
- }
- },
- child: BlocBuilder(
- builder: (context, financingState) {
- final formCubit = context.watch();
- final formState = formCubit.state;
+ final ResultSharer sharer;
- void calculate() {
- if (formState.canCalculate) {
- context.read().calculate(formCubit.financing);
- }
- }
-
- return FormsView(
- fieldSpecs: _fieldSpecs,
- controllerFor: formCubit.controllerFor,
- stateOf: formState.stateOf,
- isLoading: financingState is FutureValueLoading,
- errorMessage: financingState is FutureValueError
- ? financingState.message ?? 'Erro ao calcular.'
- : null,
- onCalculate: calculate,
- onClear: formCubit.clear,
- onFieldTap: (field) {
- if (formState.stateOf(field) == InputFieldState.highlighted) {
- calculate();
- }
- },
- );
- },
- ),
- );
- }
+ const FutureValuePage({
+ super.key,
+ this.sharer = const SharePlusResultSharer(),
+ });
@override
Widget build(BuildContext context) {
- return Scaffold(
- appBar: PageHeader(title: 'Valor Futuro'),
- body: SafeArea(
- child: SingleChildScrollView(
- padding: const EdgeInsets.symmetric(
- horizontal: AppSpacing.md + 4,
- vertical: AppSpacing.md + 4,
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- spacing: AppSpacing.lg,
- children: [
- const TitleWidget(
- title: 'Valor Futuro',
- subtitle: 'Calcule o valor futuro do seu capital.',
- ),
- const HelpCard(
- message:
- 'Preencha 3 campos e toque no 4° para calcular automaticamente',
- ),
- _buildForms(),
- ],
- ),
+ return CalculatorScope(
+ kind: CalculatorKind.futureValue,
+ child: BlocConsumer(
+ listener: (context, state) {
+ final formCubit = context.read();
+ if (state is FutureValueCalculated) {
+ formCubit.applyResult(state.value, state.calculatedField);
+ } else if (state is FutureValueError) {
+ formCubit.invalidateCalculatedField();
+ }
+ },
+ builder: (context, state) => CalculatorFormView(
+ kind: CalculatorKind.futureValue,
+ title: title,
+ fields: fields,
+ statement: futureValueStatement,
+ missingMessage: futureValueMissingFieldsMessage,
+ summary: futureValueResultSummary,
+ onCalculate: context.read().calculate,
+ shareText: futureValueShareText,
+ sharer: sharer,
+ errorMessage: state is FutureValueError
+ ? state.message ?? 'Confira os valores.'
+ : null,
),
),
);
diff --git a/lib/modules/financial_calculators/presentation/pages/regular_deposits_page.dart b/lib/modules/financial_calculators/presentation/pages/regular_deposits_page.dart
index 489d62a..b6c097a 100644
--- a/lib/modules/financial_calculators/presentation/pages/regular_deposits_page.dart
+++ b/lib/modules/financial_calculators/presentation/pages/regular_deposits_page.dart
@@ -1,122 +1,92 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:real_calc/core/themes/spacing.dart';
-import 'package:real_calc/core/widgets/page_header.dart';
-import 'package:real_calc/core/widgets/title_widget.dart';
-import 'package:real_calc/core/widgets/help_card.dart';
-import '../../../../core/utils/decimal_input_formatter.dart';
+import 'package:real_calc/core/services/result_sharer.dart';
+import 'package:real_calc/core/services/share_plus_result_sharer.dart';
+import 'package:real_calc/core/themes/calculator_palette.dart';
+import 'package:real_calc/core/themes/calculator_scope.dart';
+import 'package:real_calc/core/utils/decimal_input_formatter.dart';
+
import '../../domain/enum/financial_calculation_target.dart';
-import '../../../../core/widgets/types/input_field_state.dart';
import '../cubit/forms/financing_form_cubit.dart';
import '../cubit/regular_deposits/regular_deposits_cubit.dart';
-import '../models/field_spec.dart';
-import '../widgets/forms_view.dart';
+import '../utils/deposits_statement.dart';
+import '../utils/result_summaries.dart';
+import '../utils/share_texts.dart';
+import '../widgets/calculator_form_view.dart';
class RegularDepositsPage extends StatelessWidget {
- static final List> _fieldSpecs = [
- FieldSpec(
- field: FinancialCalculationTarget.initialValue,
- label: 'Valor do depósito (R\$)',
+ static const title = 'Depósitos Regulares';
+
+ static final fields = [
+ CalculatorFieldConfig(
+ target: FinancialCalculationTarget.initialValue,
+ label: 'Depósito',
+ prefix: 'R\$',
+ secondLine: 'todo mês',
hint: '0,00',
- icon: Icons.savings_outlined,
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
formatters: [DecimalInputFormatter()],
),
- FieldSpec(
- field: FinancialCalculationTarget.periods,
- label: 'Prazo (meses)',
+ CalculatorFieldConfig(
+ target: FinancialCalculationTarget.periods,
+ label: 'Prazo',
+ unit: 'meses',
hint: '0',
- icon: Icons.calendar_today_outlined,
+ keyboardType: TextInputType.number,
formatters: [FilteringTextInputFormatter.digitsOnly],
),
- FieldSpec(
- field: FinancialCalculationTarget.interestRate,
- label: 'Taxa de juros (% ao mês)',
+ CalculatorFieldConfig(
+ target: FinancialCalculationTarget.interestRate,
+ label: 'Taxa de juros',
+ unit: '% a.m.',
hint: '0,00',
- icon: Icons.percent,
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
formatters: [DecimalInputFormatter()],
),
- FieldSpec(
- field: FinancialCalculationTarget.finalValue,
+ CalculatorFieldConfig(
+ target: FinancialCalculationTarget.finalValue,
label: 'Valor final',
+ prefix: 'R\$',
hint: '0,00',
- icon: Icons.wallet_outlined,
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
formatters: [DecimalInputFormatter()],
),
];
- const RegularDepositsPage({super.key});
-
- BlocListener _buildForms() {
- return BlocListener(
- listener: (context, state) {
- final formCubit = context.read();
- if (state is RegularDepositsCalculated) {
- formCubit.applyResult(state.value, state.calculatedField);
- } else if (state is RegularDepositsError) {
- formCubit.invalidateCalculatedField();
- }
- },
- child: BlocBuilder(
- builder: (context, financingState) {
- final formCubit = context.watch();
- final formState = formCubit.state;
+ final ResultSharer sharer;
- void calculate() {
- if (formState.canCalculate) {
- context.read().calculate(
- formCubit.financing,
- );
- }
- }
-
- return FormsView(
- fieldSpecs: _fieldSpecs,
- controllerFor: formCubit.controllerFor,
- stateOf: formState.stateOf,
- isLoading: financingState is RegularDepositsLoading,
- errorMessage: financingState is RegularDepositsError
- ? financingState.message ?? 'Erro ao calcular.'
- : null,
- onCalculate: calculate,
- onClear: formCubit.clear,
- onFieldTap: (field) {
- if (formState.stateOf(field) == InputFieldState.highlighted) {
- calculate();
- }
- },
- );
- },
- ),
- );
- }
+ const RegularDepositsPage({
+ super.key,
+ this.sharer = const SharePlusResultSharer(),
+ });
@override
Widget build(BuildContext context) {
- return Scaffold(
- appBar: PageHeader(title: 'Depósitos Regulares'),
- body: SafeArea(
- child: SingleChildScrollView(
- padding: const EdgeInsets.symmetric(
- horizontal: AppSpacing.md + 4,
- vertical: AppSpacing.md + 4,
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- spacing: AppSpacing.lg,
- children: [
- const TitleWidget(
- title: 'Depósitos Regulares',
- subtitle:
- 'Calcule o valor futuro, a taxa, o prazo ou o depósito mensal.',
- ),
- const HelpCard(
- message:
- 'Preencha 3 campos e toque no 4° para calcular automaticamente',
- ),
- _buildForms(),
- ],
- ),
+ return CalculatorScope(
+ kind: CalculatorKind.deposits,
+ child: BlocConsumer(
+ listener: (context, state) {
+ final formCubit = context.read();
+ if (state is RegularDepositsCalculated) {
+ formCubit.applyResult(state.value, state.calculatedField);
+ } else if (state is RegularDepositsError) {
+ formCubit.invalidateCalculatedField();
+ }
+ },
+ builder: (context, state) => CalculatorFormView(
+ kind: CalculatorKind.deposits,
+ title: title,
+ fields: fields,
+ statement: depositsStatement,
+ missingMessage: depositsMissingFieldsMessage,
+ summary: regularDepositsResultSummary,
+ onCalculate: context.read().calculate,
+ shareText: depositsShareText,
+ sharer: sharer,
+ errorMessage: state is RegularDepositsError
+ ? state.message ?? 'Confira os valores.'
+ : null,
),
),
);
diff --git a/lib/modules/financial_calculators/presentation/utils/calculator_statement.dart b/lib/modules/financial_calculators/presentation/utils/calculator_statement.dart
new file mode 100644
index 0000000..e5ac642
--- /dev/null
+++ b/lib/modules/financial_calculators/presentation/utils/calculator_statement.dart
@@ -0,0 +1,80 @@
+import 'package:intl/intl.dart';
+import 'package:real_calc/core/utils/br_number_format.dart';
+import 'package:real_calc/core/widgets/calculator_band/band_statement.dart';
+
+import '../../domain/enum/financial_calculation_target.dart';
+
+const calculatorFieldOrder = [
+ FinancialCalculationTarget.initialValue,
+ FinancialCalculationTarget.periods,
+ FinancialCalculationTarget.interestRate,
+ FinancialCalculationTarget.finalValue,
+];
+
+typedef FieldName = String Function(FinancialCalculationTarget field);
+
+final _money = NumberFormat('#,##0.00', 'pt_BR');
+
+String moneyValue(double value) => _money.format(toCents(value) / 100);
+
+String money(double value) => formatCents(toCents(value));
+
+String months(int count) => count == 1 ? '1 mês' : '$count meses';
+
+BandStatement pendingStatement(
+ Set emptyFields,
+ FieldName fieldName,
+) {
+ if (emptyFields.length == calculatorFieldOrder.length) {
+ return const BandStatement(
+ kicker: 'Informe três valores',
+ value: 'e descubra o quarto',
+ sub: 'Qualquer campo pode ser o resultado.',
+ );
+ }
+
+ if (emptyFields.length == 1) {
+ return BandStatement(
+ kicker: 'Você quer descobrir',
+ value: fieldName(emptyFields.single),
+ sub: 'Os outros três campos já estão preenchidos.',
+ );
+ }
+
+ if (emptyFields.isEmpty) {
+ return const BandStatement(
+ kicker: 'Para calcular, deixe',
+ value: '1 campo em branco',
+ sub: 'Apague o valor que você quer descobrir.',
+ );
+ }
+
+ final missing = emptyFields.length - 1;
+ return BandStatement(
+ kicker: 'Para calcular, ainda',
+ value: missing == 1 ? 'falta 1 campo' : 'faltam $missing campos',
+ sub: 'Deixe em branco só o que você quer descobrir.',
+ );
+}
+
+String missingFieldsMessage(
+ Set emptyFields,
+ FieldName fieldName,
+) {
+ final names = [
+ for (final field in calculatorFieldOrder)
+ if (emptyFields.contains(field)) fieldName(field),
+ ];
+ final list = names.length == 1
+ ? names.single
+ : '${names.sublist(0, names.length - 1).join(', ')} ou ${names.last}';
+ return 'Preencha $list. O campo que ficar em branco é calculado.';
+}
+
+BandStatement rateStatement(double monthlyRate, {required String kicker}) {
+ return BandStatement(
+ kicker: kicker,
+ value: '${formatPercent(monthlyRate)} ao mês',
+ sub: '${formatPercent(annualEquivalentRate(monthlyRate))} ao ano',
+ );
+}
diff --git a/lib/modules/financial_calculators/presentation/utils/deposits_statement.dart b/lib/modules/financial_calculators/presentation/utils/deposits_statement.dart
new file mode 100644
index 0000000..2c95ea8
--- /dev/null
+++ b/lib/modules/financial_calculators/presentation/utils/deposits_statement.dart
@@ -0,0 +1,57 @@
+import 'package:real_calc/core/utils/br_number_format.dart';
+import 'package:real_calc/core/widgets/calculator_band/band_statement.dart';
+
+import '../../domain/entities/financial_calculation.dart';
+import '../../domain/enum/financial_calculation_target.dart';
+import 'calculator_statement.dart';
+
+String depositsFieldName(FinancialCalculationTarget field) => switch (field) {
+ FinancialCalculationTarget.initialValue => 'o depósito',
+ FinancialCalculationTarget.periods => 'o prazo',
+ FinancialCalculationTarget.interestRate => 'a taxa de juros',
+ FinancialCalculationTarget.finalValue => 'o valor final',
+};
+
+BandStatement depositsStatement({
+ required Set emptyFields,
+ required FinancialCalculation values,
+ FinancialCalculationTarget? calculatedField,
+}) {
+ if (calculatedField == null) {
+ return pendingStatement(emptyFields, depositsFieldName);
+ }
+
+ final deposit = money(values.initialValue);
+ final total = money(values.finalValue);
+ final yieldCents =
+ toCents(values.finalValue) -
+ toCents(values.initialValue) * values.periods;
+ final yieldText = '${formatCents(yieldCents)} de rendimento';
+
+ return switch (calculatedField) {
+ FinancialCalculationTarget.finalValue => BandStatement(
+ kicker: 'Guardando $deposit por mês, você terá',
+ value: moneyValue(values.finalValue),
+ isMoney: true,
+ sub: 'em ${months(values.periods)}, com $yieldText',
+ ),
+ FinancialCalculationTarget.initialValue => BandStatement(
+ kicker: 'Para juntar $total, guarde por mês',
+ value: moneyValue(values.initialValue),
+ isMoney: true,
+ sub: 'durante ${months(values.periods)}',
+ ),
+ FinancialCalculationTarget.periods => BandStatement(
+ kicker: 'Guardando $deposit por mês, você junta $total em',
+ value: months(values.periods),
+ sub: 'com $yieldText',
+ ),
+ FinancialCalculationTarget.interestRate => rateStatement(
+ values.rate,
+ kicker: 'Para isso, o dinheiro precisa render',
+ ),
+ };
+}
+
+String depositsMissingFieldsMessage(Set empty) =>
+ missingFieldsMessage(empty, depositsFieldName);
diff --git a/lib/modules/financial_calculators/presentation/utils/financing_statement.dart b/lib/modules/financial_calculators/presentation/utils/financing_statement.dart
new file mode 100644
index 0000000..7871fe7
--- /dev/null
+++ b/lib/modules/financial_calculators/presentation/utils/financing_statement.dart
@@ -0,0 +1,56 @@
+import 'package:real_calc/core/widgets/calculator_band/band_statement.dart';
+
+import '../../domain/entities/financial_calculation.dart';
+import '../../domain/enum/financial_calculation_target.dart';
+import 'calculator_statement.dart';
+
+String financingFieldName(FinancialCalculationTarget field) => switch (field) {
+ FinancialCalculationTarget.initialValue => 'o valor financiado',
+ FinancialCalculationTarget.periods => 'o prazo',
+ FinancialCalculationTarget.interestRate => 'a taxa de juros',
+ FinancialCalculationTarget.finalValue => 'a prestação',
+};
+
+BandStatement financingStatement({
+ required Set emptyFields,
+ required FinancialCalculation values,
+ FinancialCalculationTarget? calculatedField,
+}) {
+ if (calculatedField == null) {
+ return pendingStatement(emptyFields, financingFieldName);
+ }
+
+ final installment = money(values.finalValue);
+ final periods = values.periods;
+
+ return switch (calculatedField) {
+ FinancialCalculationTarget.finalValue => BandStatement(
+ kicker: 'Sua prestação vai ser',
+ value: moneyValue(values.finalValue),
+ isMoney: true,
+ sub: periods == 1
+ ? '1 parcela, pela Tabela Price'
+ : '$periods parcelas iguais, pela Tabela Price',
+ ),
+ FinancialCalculationTarget.initialValue => BandStatement(
+ kicker: 'Você pode financiar',
+ value: moneyValue(values.initialValue),
+ isMoney: true,
+ sub: periods == 1
+ ? 'em 1 parcela de $installment'
+ : 'em $periods parcelas de $installment',
+ ),
+ FinancialCalculationTarget.periods => BandStatement(
+ kicker: 'Você quita em',
+ value: months(periods),
+ sub: 'com parcelas de $installment',
+ ),
+ FinancialCalculationTarget.interestRate => rateStatement(
+ values.rate,
+ kicker: 'A taxa de juros é de',
+ ),
+ };
+}
+
+String financingMissingFieldsMessage(Set empty) =>
+ missingFieldsMessage(empty, financingFieldName);
diff --git a/lib/modules/financial_calculators/presentation/utils/future_value_statement.dart b/lib/modules/financial_calculators/presentation/utils/future_value_statement.dart
new file mode 100644
index 0000000..3843190
--- /dev/null
+++ b/lib/modules/financial_calculators/presentation/utils/future_value_statement.dart
@@ -0,0 +1,56 @@
+import 'package:real_calc/core/utils/br_number_format.dart';
+import 'package:real_calc/core/widgets/calculator_band/band_statement.dart';
+
+import '../../domain/entities/financial_calculation.dart';
+import '../../domain/enum/financial_calculation_target.dart';
+import 'calculator_statement.dart';
+
+String futureValueFieldName(FinancialCalculationTarget field) =>
+ switch (field) {
+ FinancialCalculationTarget.initialValue => 'o valor aplicado',
+ FinancialCalculationTarget.periods => 'o prazo',
+ FinancialCalculationTarget.interestRate => 'a taxa de juros',
+ FinancialCalculationTarget.finalValue => 'o valor futuro',
+ };
+
+BandStatement futureValueStatement({
+ required Set emptyFields,
+ required FinancialCalculation values,
+ FinancialCalculationTarget? calculatedField,
+}) {
+ if (calculatedField == null) {
+ return pendingStatement(emptyFields, futureValueFieldName);
+ }
+
+ final applied = money(values.initialValue);
+ final future = money(values.finalValue);
+ final yieldCents = toCents(values.finalValue) - toCents(values.initialValue);
+ final yieldText = '${formatCents(yieldCents)} de rendimento';
+
+ return switch (calculatedField) {
+ FinancialCalculationTarget.finalValue => BandStatement(
+ kicker: 'Aplicando $applied hoje, você terá',
+ value: moneyValue(values.finalValue),
+ isMoney: true,
+ sub: 'em ${months(values.periods)}, com $yieldText',
+ ),
+ FinancialCalculationTarget.initialValue => BandStatement(
+ kicker: 'Para ter $future, aplique hoje',
+ value: moneyValue(values.initialValue),
+ isMoney: true,
+ sub: 'por ${months(values.periods)}',
+ ),
+ FinancialCalculationTarget.periods => BandStatement(
+ kicker: '$applied viram $future em',
+ value: months(values.periods),
+ sub: 'com $yieldText',
+ ),
+ FinancialCalculationTarget.interestRate => rateStatement(
+ values.rate,
+ kicker: 'Para isso, o dinheiro precisa render',
+ ),
+ };
+}
+
+String futureValueMissingFieldsMessage(Set empty) =>
+ missingFieldsMessage(empty, futureValueFieldName);
diff --git a/lib/modules/financial_calculators/presentation/utils/result_summaries.dart b/lib/modules/financial_calculators/presentation/utils/result_summaries.dart
new file mode 100644
index 0000000..a1a52d1
--- /dev/null
+++ b/lib/modules/financial_calculators/presentation/utils/result_summaries.dart
@@ -0,0 +1,79 @@
+import 'package:real_calc/core/utils/br_number_format.dart';
+import 'package:real_calc/core/widgets/result_details/result_summary.dart';
+
+import '../../domain/entities/financial_calculation.dart';
+
+// Os valores derivados saem dos valores arredondados que a pessoa vê na tela
+// (em centavos), para a conta fechar: 236,40 × 12 = 2.836,80.
+
+/// Financiamento: [FinancialCalculation.initialValue] é o valor emprestado e
+/// [FinancialCalculation.finalValue], a prestação.
+ResultSummary financingResultSummary(FinancialCalculation calculation) {
+ final borrowed = toCents(calculation.initialValue);
+ final total = toCents(calculation.finalValue) * calculation.periods;
+ final interest = total - borrowed;
+
+ return ResultSummary(
+ title: 'Do total que você vai pagar',
+ composition: ResultComposition.fromCents(
+ ownLabel: 'Emprestado',
+ ownCents: borrowed,
+ otherLabel: 'Juros',
+ otherCents: interest,
+ ),
+ lines: [
+ ResultDetailLine('Total pago', formatCents(total)),
+ ResultDetailLine('Total de juros', formatCents(interest)),
+ _annualRateLine(calculation.rate),
+ ],
+ );
+}
+
+/// Depósitos Regulares: [FinancialCalculation.initialValue] é o depósito
+/// mensal e [FinancialCalculation.finalValue], o valor final.
+ResultSummary regularDepositsResultSummary(FinancialCalculation calculation) {
+ final deposited = toCents(calculation.initialValue) * calculation.periods;
+ final yield_ = toCents(calculation.finalValue) - deposited;
+
+ return ResultSummary(
+ title: 'Do valor final',
+ composition: ResultComposition.fromCents(
+ ownLabel: 'Depositado',
+ ownCents: deposited,
+ otherLabel: 'Rendimento',
+ otherCents: yield_,
+ ),
+ lines: [
+ ResultDetailLine('Total depositado', formatCents(deposited)),
+ ResultDetailLine('Rendimento', formatCents(yield_)),
+ _annualRateLine(calculation.rate),
+ ],
+ );
+}
+
+/// Valor Futuro: [FinancialCalculation.initialValue] é o valor aplicado e
+/// [FinancialCalculation.finalValue], o valor final.
+ResultSummary futureValueResultSummary(FinancialCalculation calculation) {
+ final applied = toCents(calculation.initialValue);
+ final yield_ = toCents(calculation.finalValue) - applied;
+
+ return ResultSummary(
+ title: 'Do valor futuro',
+ composition: ResultComposition.fromCents(
+ ownLabel: 'Valor aplicado',
+ ownCents: applied,
+ otherLabel: 'Rendimento',
+ otherCents: yield_,
+ ),
+ lines: [
+ ResultDetailLine('Valor aplicado', formatCents(applied)),
+ ResultDetailLine('Rendimento', formatCents(yield_)),
+ _annualRateLine(calculation.rate),
+ ],
+ );
+}
+
+ResultDetailLine _annualRateLine(double monthlyPercent) => ResultDetailLine(
+ 'Taxa anual equivalente',
+ '${formatPercent(annualEquivalentRate(monthlyPercent))} a.a.',
+);
diff --git a/lib/modules/financial_calculators/presentation/utils/share_texts.dart b/lib/modules/financial_calculators/presentation/utils/share_texts.dart
new file mode 100644
index 0000000..e2d25a2
--- /dev/null
+++ b/lib/modules/financial_calculators/presentation/utils/share_texts.dart
@@ -0,0 +1,113 @@
+import 'package:real_calc/core/utils/br_number_format.dart';
+import 'package:real_calc/core/utils/share_text.dart';
+
+import '../../domain/entities/financial_calculation.dart';
+import '../../domain/enum/financial_calculation_target.dart';
+import 'calculator_statement.dart';
+import 'deposits_statement.dart';
+import 'financing_statement.dart';
+import 'future_value_statement.dart';
+
+typedef ShareTextBuilder =
+ String Function(
+ FinancialCalculation values,
+ FinancialCalculationTarget calculatedField,
+ );
+
+List<(String, String)> _fieldLines(
+ FinancialCalculation values,
+ FinancialCalculationTarget calculatedField,
+ Map labels,
+) => [
+ for (final field in calculatorFieldOrder)
+ if (field != calculatedField)
+ (
+ labels[field]!,
+ switch (field) {
+ FinancialCalculationTarget.initialValue => money(values.initialValue),
+ FinancialCalculationTarget.periods => months(values.periods),
+ FinancialCalculationTarget.interestRate =>
+ '${formatPercent(values.rate)} ao mês',
+ FinancialCalculationTarget.finalValue => money(values.finalValue),
+ },
+ ),
+];
+
+String financingShareText(
+ FinancialCalculation values,
+ FinancialCalculationTarget calculatedField,
+) {
+ final total = toCents(values.finalValue) * values.periods;
+ final interest = total - toCents(values.initialValue);
+
+ return shareText(
+ calculator: 'Financiamento',
+ statement: financingStatement(
+ emptyFields: const {},
+ values: values,
+ calculatedField: calculatedField,
+ ),
+ lines: [
+ ..._fieldLines(values, calculatedField, const {
+ FinancialCalculationTarget.initialValue: 'Valor financiado',
+ FinancialCalculationTarget.periods: 'Prazo',
+ FinancialCalculationTarget.interestRate: 'Taxa de juros',
+ FinancialCalculationTarget.finalValue: 'Prestação',
+ }),
+ ('Total pago', formatCents(total)),
+ ('Juros', formatCents(interest)),
+ ],
+ );
+}
+
+String depositsShareText(
+ FinancialCalculation values,
+ FinancialCalculationTarget calculatedField,
+) {
+ final deposited = toCents(values.initialValue) * values.periods;
+ final yield_ = toCents(values.finalValue) - deposited;
+
+ return shareText(
+ calculator: 'Depósitos Regulares',
+ statement: depositsStatement(
+ emptyFields: const {},
+ values: values,
+ calculatedField: calculatedField,
+ ),
+ lines: [
+ ..._fieldLines(values, calculatedField, const {
+ FinancialCalculationTarget.initialValue: 'Depósito mensal',
+ FinancialCalculationTarget.periods: 'Prazo',
+ FinancialCalculationTarget.interestRate: 'Taxa de juros',
+ FinancialCalculationTarget.finalValue: 'Valor final',
+ }),
+ ('Total depositado', formatCents(deposited)),
+ ('Rendimento', formatCents(yield_)),
+ ],
+ );
+}
+
+String futureValueShareText(
+ FinancialCalculation values,
+ FinancialCalculationTarget calculatedField,
+) {
+ final yield_ = toCents(values.finalValue) - toCents(values.initialValue);
+
+ return shareText(
+ calculator: 'Valor Futuro',
+ statement: futureValueStatement(
+ emptyFields: const {},
+ values: values,
+ calculatedField: calculatedField,
+ ),
+ lines: [
+ ..._fieldLines(values, calculatedField, const {
+ FinancialCalculationTarget.initialValue: 'Valor aplicado',
+ FinancialCalculationTarget.periods: 'Prazo',
+ FinancialCalculationTarget.interestRate: 'Taxa de juros',
+ FinancialCalculationTarget.finalValue: 'Valor futuro',
+ }),
+ ('Rendimento', formatCents(yield_)),
+ ],
+ );
+}
diff --git a/lib/modules/financial_calculators/presentation/widgets/calculator_form_view.dart b/lib/modules/financial_calculators/presentation/widgets/calculator_form_view.dart
new file mode 100644
index 0000000..7fd0c57
--- /dev/null
+++ b/lib/modules/financial_calculators/presentation/widgets/calculator_form_view.dart
@@ -0,0 +1,195 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:real_calc/core/services/result_sharer.dart';
+import 'package:real_calc/core/themes/calculator_palette.dart';
+import 'package:real_calc/core/themes/spacing.dart';
+import 'package:real_calc/core/widgets/bottom_action_bar.dart';
+import 'package:real_calc/core/widgets/calculator_band/band_statement.dart';
+import 'package:real_calc/core/widgets/calculator_band/calculator_band.dart';
+import 'package:real_calc/core/widgets/field_list/field_list.dart';
+import 'package:real_calc/core/widgets/field_list/field_row.dart';
+import 'package:real_calc/core/widgets/form_notice.dart';
+import 'package:real_calc/core/widgets/help_sheet/help_sheet.dart';
+import 'package:real_calc/core/widgets/result_details/result_details.dart';
+import 'package:real_calc/core/widgets/result_details/result_summary.dart';
+
+import '../../domain/entities/financial_calculation.dart';
+import '../../domain/enum/financial_calculation_target.dart';
+import '../cubit/forms/financing_form_cubit.dart';
+import '../utils/share_texts.dart';
+
+class CalculatorFieldConfig {
+ final FinancialCalculationTarget target;
+ final String label;
+ final String hint;
+ final TextInputType keyboardType;
+ final List formatters;
+ final String? prefix;
+ final String? unit;
+ final String? secondLine;
+
+ const CalculatorFieldConfig({
+ required this.target,
+ required this.label,
+ required this.hint,
+ required this.keyboardType,
+ required this.formatters,
+ this.prefix,
+ this.unit,
+ this.secondLine,
+ });
+}
+
+typedef StatementBuilder =
+ BandStatement Function({
+ required Set emptyFields,
+ required FinancialCalculation values,
+ FinancialCalculationTarget? calculatedField,
+ });
+
+class CalculatorFormView extends StatelessWidget {
+ final CalculatorKind kind;
+ final String title;
+ final List fields;
+ final StatementBuilder statement;
+ final String Function(Set) missingMessage;
+ final ResultSummary Function(FinancialCalculation) summary;
+ final ValueChanged onCalculate;
+ final ShareTextBuilder shareText;
+ final ResultSharer sharer;
+ final String? errorMessage;
+
+ const CalculatorFormView({
+ super.key,
+ required this.kind,
+ required this.title,
+ required this.fields,
+ required this.statement,
+ required this.missingMessage,
+ required this.summary,
+ required this.onCalculate,
+ required this.shareText,
+ required this.sharer,
+ this.errorMessage,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final formCubit = context.watch();
+ final formState = formCubit.state;
+
+ final emptyFields = {
+ for (final field in FinancialCalculationTarget.values)
+ if (formCubit.controllerFor(field).text.trim().isEmpty) field,
+ };
+ final calculatedField = formState.calculatedField;
+ final values = formCubit.financing;
+ final filled =
+ FinancialCalculationTarget.values.length - emptyFields.length;
+
+ void calculate() {
+ if (formCubit.state.canCalculate) onCalculate(formCubit.financing);
+ }
+
+ void clear() {
+ final snapshot = {
+ for (final field in FinancialCalculationTarget.values)
+ field: formCubit.controllerFor(field).text,
+ };
+ final previouslyCalculated = formCubit.state.calculatedField;
+ FocusScope.of(context).unfocus();
+ formCubit.clear();
+ showFieldsClearedSnackBar(
+ context,
+ onUndo: () {
+ for (final MapEntry(key: field, value: text) in snapshot.entries) {
+ if (field != previouslyCalculated) {
+ formCubit.controllerFor(field).text = text;
+ }
+ }
+ if (previouslyCalculated != null) calculate();
+ },
+ );
+ }
+
+ return Scaffold(
+ body: Column(
+ children: [
+ CalculatorBand(
+ title: title,
+ statement: statement(
+ emptyFields: emptyFields,
+ values: values,
+ calculatedField: calculatedField,
+ ),
+ onHelp: () => showHelpSheet(context, kind),
+ onClear: clear,
+ ),
+ Expanded(
+ child: SingleChildScrollView(
+ padding: const EdgeInsets.fromLTRB(
+ AppSpacing.screen,
+ AppSpacing.sm,
+ AppSpacing.screen,
+ AppSpacing.lg,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ FieldList(
+ children: [
+ for (final field in fields)
+ FieldRow(
+ key: ValueKey(field.target),
+ label: field.label,
+ controller: formCubit.controllerFor(field.target),
+ state: formState.stateOf(field.target),
+ prefix: field.prefix,
+ unit: field.unit,
+ secondLine: field.secondLine,
+ hint: field.hint,
+ keyboardType: field.keyboardType,
+ inputFormatters: field.formatters,
+ onCalculate: calculate,
+ ),
+ ],
+ ),
+ if (errorMessage != null)
+ Padding(
+ padding: const EdgeInsets.only(top: AppSpacing.md),
+ child: FormNotice(
+ title: 'Não foi possível calcular',
+ message: errorMessage!,
+ isError: true,
+ ),
+ )
+ else if (filled > 0 && filled < 3)
+ Padding(
+ padding: const EdgeInsets.only(top: AppSpacing.md),
+ child: FormNotice(
+ title: 'Faltam dados para calcular',
+ message: missingMessage(emptyFields),
+ ),
+ ),
+ if (calculatedField != null)
+ ResultDetails(summary: summary(values)),
+ ],
+ ),
+ ),
+ ),
+ BottomActionBar(
+ primaryLabel: 'Calcular',
+ onPrimary: formState.canCalculate ? calculate : null,
+ onShare: calculatedField == null
+ ? null
+ : (origin) => sharer.share(
+ shareText(values, calculatedField),
+ origin: origin,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/modules/financial_calculators/presentation/widgets/forms_view.dart b/lib/modules/financial_calculators/presentation/widgets/forms_view.dart
deleted file mode 100644
index 82ac9af..0000000
--- a/lib/modules/financial_calculators/presentation/widgets/forms_view.dart
+++ /dev/null
@@ -1,99 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:real_calc/core/themes/extensions/financing_forms_theme.dart';
-import 'package:real_calc/core/themes/spacing.dart';
-import 'package:real_calc/core/widgets/types/input_field_state.dart';
-import 'package:real_calc/core/widgets/options_bottom_forms.dart';
-import '../models/field_spec.dart';
-import '../../../../core/widgets/input_forms_result_card.dart';
-
-class FormsView extends StatelessWidget {
- final List> fieldSpecs;
- final TextEditingController Function(T field) controllerFor;
- final InputFieldState Function(T field) stateOf;
- final bool isLoading;
- final String? errorMessage;
- final VoidCallback? onCalculate;
- final VoidCallback? onClear;
- final ValueChanged? onFieldTap;
-
- const FormsView({
- required this.fieldSpecs,
- required this.controllerFor,
- required this.stateOf,
- this.isLoading = false,
- this.errorMessage,
- this.onCalculate,
- this.onClear,
- this.onFieldTap,
- super.key,
- });
-
- @override
- Widget build(BuildContext context) {
- final styles = Theme.of(context).extension()!;
-
- return Form(
- child: Column(
- children: [
- if (errorMessage != null) ...[
- _ErrorBanner(message: errorMessage!, styles: styles),
- SizedBox(height: AppSpacing.md),
- ],
- if (isLoading)
- Padding(
- padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm),
- child: LinearProgressIndicator(
- color: styles.progressIndicatorColor,
- ),
- ),
- for (final spec in fieldSpecs) ...[
- InputFormsResultCard(
- label: spec.label,
- hint: spec.hint,
- icon: spec.icon,
- controller: controllerFor(spec.field),
- inputFormatters: spec.formatters,
- state: stateOf(spec.field),
- onTap: () => onFieldTap?.call(spec.field),
- ),
- SizedBox(height: AppSpacing.md),
- ],
- SizedBox(height: AppSpacing.sm),
- OptionsBottomForms(onCalculate: onCalculate, onClear: onClear),
- ],
- ),
- );
- }
-}
-
-class _ErrorBanner extends StatelessWidget {
- final String message;
- final FinancingFormsTheme styles;
-
- const _ErrorBanner({required this.message, required this.styles});
-
- @override
- Widget build(BuildContext context) {
- return Container(
- width: double.infinity,
- padding: styles.errorContainerPadding,
- decoration: BoxDecoration(
- color: styles.errorBackgroundColor,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: styles.errorBorderColor),
- ),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Icon(
- Icons.warning_amber_rounded,
- size: 18,
- color: styles.errorBorderColor,
- ),
- const SizedBox(width: 10),
- Expanded(child: Text(message, style: styles.errorTextStyle)),
- ],
- ),
- );
- }
-}
diff --git a/lib/modules/home/domain/enum/menu_card_variant.dart b/lib/modules/home/domain/enum/menu_card_variant.dart
deleted file mode 100644
index 591ec35..0000000
--- a/lib/modules/home/domain/enum/menu_card_variant.dart
+++ /dev/null
@@ -1,12 +0,0 @@
-enum MenuCardVariant {
- /// Card quadrado usado no grid 2 colunas — mesma lógica de antes,
- /// só sem a caixinha colorida atrás do ícone.
- standard,
-
- /// Card quadrado usado no grid 2 colunas, mas com a caixinha colorida
- /// atrás do ícone.
- featured,
-
- /// Card quadrado usado no grid 2 colunas, mas com o card desabilitado.
- disabled,
-}
\ No newline at end of file
diff --git a/lib/modules/home/domain/enum/metric_status.dart b/lib/modules/home/domain/enum/metric_status.dart
deleted file mode 100644
index 3768a1d..0000000
--- a/lib/modules/home/domain/enum/metric_status.dart
+++ /dev/null
@@ -1,5 +0,0 @@
-enum MetricStatus {
- loading,
- data,
- offline
-}
\ No newline at end of file
diff --git a/lib/modules/home/domain/enum/metric_trend.dart b/lib/modules/home/domain/enum/metric_trend.dart
deleted file mode 100644
index f7daffb..0000000
--- a/lib/modules/home/domain/enum/metric_trend.dart
+++ /dev/null
@@ -1,5 +0,0 @@
-enum MetricTrend {
- up,
- down,
- flat
-}
\ No newline at end of file
diff --git a/lib/modules/home/presentation/components/calculator_list_item.dart b/lib/modules/home/presentation/components/calculator_list_item.dart
new file mode 100644
index 0000000..7458bc0
--- /dev/null
+++ b/lib/modules/home/presentation/components/calculator_list_item.dart
@@ -0,0 +1,106 @@
+import 'package:flutter/material.dart';
+import 'package:real_calc/core/widgets/app_icon.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:real_calc/core/themes/app_colors.dart';
+import 'package:real_calc/core/themes/calculator_palette.dart';
+import 'package:real_calc/core/themes/radius.dart';
+import 'package:real_calc/core/themes/spacing.dart';
+import 'package:real_calc/core/themes/text_styles.dart';
+
+class CalculatorListItem extends StatelessWidget {
+ static const double iconTileSize = 40;
+
+ final CalculatorKind kind;
+ final String iconAsset;
+ final String title;
+ final String description;
+ final VoidCallback onTap;
+
+ const CalculatorListItem({
+ super.key,
+ required this.kind,
+ required this.iconAsset,
+ required this.title,
+ required this.description,
+ required this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final colors = AppColors.of(context);
+ final palette = CalculatorPalette.of(
+ kind,
+ brightness: Theme.of(context).brightness,
+ );
+
+ return MergeSemantics(
+ child: Semantics(
+ button: true,
+ child: InkWell(
+ onTap: onTap,
+ child: Container(
+ constraints: const BoxConstraints(
+ minHeight: AppSpacing.homeRowHeight,
+ ),
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ decoration: BoxDecoration(
+ border: Border(bottom: BorderSide(color: colors.hairline)),
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: iconTileSize,
+ height: iconTileSize,
+ decoration: BoxDecoration(
+ color: palette.solid,
+ borderRadius: BorderRadius.circular(AppRadius.iconTile),
+ ),
+ alignment: Alignment.center,
+ child: SvgPicture.asset(
+ iconAsset,
+ width: 22,
+ height: 22,
+ colorFilter: ColorFilter.mode(
+ palette.onSolid,
+ BlendMode.srcIn,
+ ),
+ ),
+ ),
+ const SizedBox(width: 14),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ title,
+ style: AppTextStyles.listTitle.copyWith(
+ color: colors.textPrimary,
+ ),
+ ),
+ const SizedBox(height: 2),
+ Text(
+ description,
+ style: AppTextStyles.listDesc.copyWith(
+ color: colors.textSecondary,
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(width: AppSpacing.sm),
+ ExcludeSemantics(
+ child: AppIcon(
+ AppIcons.chevronRight,
+ size: 20,
+ color: colors.textHint,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/modules/home/presentation/components/highlight_card.dart b/lib/modules/home/presentation/components/highlight_card.dart
deleted file mode 100644
index 9dd27c0..0000000
--- a/lib/modules/home/presentation/components/highlight_card.dart
+++ /dev/null
@@ -1,53 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:real_calc/core/themes/extensions/highlight_card_theme.dart';
-
-class HighlightCard extends StatelessWidget {
- final VoidCallback? onTap;
-
- const HighlightCard({super.key, this.onTap});
-
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context).extension()!;
-
- return Container(
- width: double.infinity,
- padding: theme.padding,
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(theme.borderRadius),
- gradient: LinearGradient(
- colors: theme.gradientColors,
- begin: theme.gradientBegin,
- end: theme.gradientEnd,
- ),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text('Selic e Índices', style: theme.titleStyle),
- const SizedBox(height: 8),
- Text(
- 'Acompanhe as taxas oficiais atualizadas diariamente pelo Banco Central.',
- style: theme.subtitleStyle.copyWith(
- color: theme.subtitleStyle.color?.withValues(alpha: 0.8),
- ),
- ),
- const SizedBox(height: 20),
- ElevatedButton(
- onPressed: onTap,
- style: ElevatedButton.styleFrom(
- backgroundColor: theme.buttonBackgroundColor.withValues(alpha: 0.2),
- foregroundColor: theme.buttonForegroundColor,
- elevation: 0,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
- ),
- padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
- ),
- child: Text('CONSULTAR TAXAS', style: theme.buttonTextStyle),
- ),
- ],
- ),
- );
- }
-}
\ No newline at end of file
diff --git a/lib/modules/home/presentation/components/menu_card.dart b/lib/modules/home/presentation/components/menu_card.dart
deleted file mode 100644
index 578a1ac..0000000
--- a/lib/modules/home/presentation/components/menu_card.dart
+++ /dev/null
@@ -1,246 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:real_calc/core/themes/extensions/menu_card_theme.dart';
-import 'package:real_calc/modules/home/domain/enum/menu_card_variant.dart';
-
-class MenuCard extends StatelessWidget {
- final IconData icon;
- final Color cardColor;
- final Color iconColor;
- final String title;
- final String description;
- final VoidCallback? onTap;
- final MenuCardVariant variant;
-
- const MenuCard({
- super.key,
- required this.icon,
- required this.cardColor,
- required this.iconColor,
- required this.title,
- required this.description,
- this.onTap,
- this.variant = MenuCardVariant.standard,
- });
-
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context).extension()!;
-
- switch (variant) {
- case MenuCardVariant.featured:
- return _FeaturedMenuCard(
- icon: icon,
- cardColor: cardColor,
- iconColor: iconColor,
- title: title,
- description: description,
- onTap: onTap,
- theme: theme,
- );
- case MenuCardVariant.disabled:
- return _DisabledMenuCard(icon: icon, title: title, theme: theme);
- case MenuCardVariant.standard:
- return _StandardMenuCard(
- icon: icon,
- cardColor: cardColor,
- iconColor: iconColor,
- title: title,
- description: description,
- onTap: onTap,
- theme: theme,
- );
- }
- }
-}
-
-/// Card quadrado usado no grid 2 colunas.
-class _StandardMenuCard extends StatelessWidget {
- final IconData icon;
- final Color cardColor;
- final Color iconColor;
- final String title;
- final String description;
- final VoidCallback? onTap;
- final MenuCardTheme theme;
-
- const _StandardMenuCard({
- required this.icon,
- required this.cardColor,
- required this.iconColor,
- required this.title,
- required this.description,
- required this.theme,
- this.onTap,
- });
-
- @override
- Widget build(BuildContext context) {
- return InkWell(
- onTap: onTap,
- borderRadius: BorderRadius.circular(theme.borderRadius),
- child: LayoutBuilder(
- builder: (context, constraints) {
- final bool telaMuitoPequena =
- constraints.maxHeight < theme.heightThreshold;
-
- return Container(
- key: const Key('menu_card_container'),
- padding: telaMuitoPequena ? theme.compactPadding : theme.padding,
- width: double.infinity,
- decoration: BoxDecoration(
- color: cardColor,
- borderRadius: BorderRadius.circular(0),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(
- icon,
- color: iconColor,
- size: telaMuitoPequena
- ? theme.iconSizeCompact
- : theme.iconSize,
- ),
- SizedBox(height: telaMuitoPequena ? 4 : 12),
- Text(
- title,
- style: telaMuitoPequena
- ? theme.titleStyleCompact
- : theme.titleStyle,
- maxLines: 1,
- softWrap: false,
- overflow: TextOverflow.ellipsis,
- ),
- Text(
- description,
- style: telaMuitoPequena
- ? theme.descriptionStyleCompact
- : theme.descriptionStyle,
- maxLines: 1,
- softWrap: false,
- overflow: TextOverflow.ellipsis,
- ),
- ],
- ),
- );
- },
- ),
- );
- }
-}
-
-/// Card full-width horizontal com borda de destaque, pra UMA ação principal.
-class _FeaturedMenuCard extends StatelessWidget {
- final IconData icon;
- final Color cardColor;
- final Color iconColor;
- final String title;
- final String description;
- final VoidCallback? onTap;
- final MenuCardTheme theme;
-
- const _FeaturedMenuCard({
- required this.icon,
- required this.cardColor,
- required this.iconColor,
- required this.title,
- required this.description,
- required this.theme,
- this.onTap,
- });
-
- @override
- Widget build(BuildContext context) {
- return InkWell(
- onTap: onTap,
- borderRadius: BorderRadius.circular(theme.borderRadius),
- child: Container(
- key: const Key('menu_card_container'),
- width: double.infinity,
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
- decoration: BoxDecoration(
- color: cardColor,
- borderRadius: BorderRadius.circular(theme.borderRadius),
- border: Border(
- left: BorderSide(color: theme.featuredBorderColor, width: 2),
- ),
- ),
- child: Row(
- children: [
- Icon(icon, color: iconColor, size: theme.iconSize),
- const SizedBox(width: 14),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(
- title,
- style: theme.titleStyleFeatured,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- const SizedBox(height: 2),
- Text(
- description,
- style: theme.descriptionStyleFeatured,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- ],
- ),
- ),
- Icon(
- Icons.chevron_right,
- color: theme.descriptionStyleFeatured.color?.withValues(alpha: 0.6),
- size: 18,
- ),
- ],
- ),
- ),
- );
- }
-}
-
-/// Card full-width horizontal, sem interação, indicando indisponibilidade.
-class _DisabledMenuCard extends StatelessWidget {
- final IconData icon;
- final String title;
- final MenuCardTheme theme;
-
- const _DisabledMenuCard({
- required this.icon,
- required this.title,
- required this.theme,
- });
-
- @override
- Widget build(BuildContext context) {
- return Container(
- key: const Key('menu_card_container'),
- width: double.infinity,
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
- decoration: BoxDecoration(
- border: Border.all(
- color: theme.disabledLabelColor.withValues(alpha: 0.15),
- ),
- borderRadius: BorderRadius.circular(theme.borderRadius),
- ),
- child: Row(
- children: [
- Icon(icon, color: theme.disabledLabelColor, size: theme.iconSizeCompact),
- const SizedBox(width: 12),
- Expanded(
- child: Text(
- '$title · em breve',
- style: theme.disabledTextStyle.copyWith(
- color: theme.disabledLabelColor,
- ),
- ),
- ),
- ],
- ),
- );
- }
-}
\ No newline at end of file
diff --git a/lib/modules/home/presentation/components/metric_card.dart b/lib/modules/home/presentation/components/metric_card.dart
deleted file mode 100644
index ecbfd0a..0000000
--- a/lib/modules/home/presentation/components/metric_card.dart
+++ /dev/null
@@ -1,434 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:real_calc/core/themes/extensions/metric_card_theme.dart';
-import 'package:real_calc/modules/home/domain/enum/metric_status.dart';
-
-import '../../domain/enum/metric_trend.dart';
-import 'sparkline_painter.dart';
-
-
-/// Card de métrica que exibe a taxa Selic atual, sua variação recente
-/// e uma mini sparkline de tendência. Componente puramente apresentacional
-/// — quem decide o status (loading/data/offline) é o cubit que consome a
-/// API do BCB; esse widget só sabe desenhar cada um dos 3 estados.
-class MetricCard extends StatelessWidget {
- final MetricStatus status;
- final double? ratePercent;
- final double? variationPercent;
- final List sparklineData;
- final String caption;
- final VoidCallback? onTap;
-
- /// Estado de carregamento — usado enquanto a requisição está em voo.
- const MetricCard.loading({super.key})
- : status = MetricStatus.loading,
- ratePercent = null,
- variationPercent = null,
- sparklineData = const [],
- caption = '',
- onTap = null;
-
- /// Estado com dado disponível (o fluxo normal).
- const MetricCard.data({
- super.key,
- required this.ratePercent,
- required this.variationPercent,
- required this.sparklineData,
- required this.caption,
- this.onTap,
- }) : status = MetricStatus.data;
-
- /// Estado sem conexão. Se `ratePercent` vier preenchido, mostra o
- /// último valor salvo em cache (esmaecido); se vier nulo, mostra um
- /// estado vazio com opção de tentar novamente via `onTap`.
- const MetricCard.offline({
- super.key,
- this.ratePercent,
- this.variationPercent,
- this.sparklineData = const [],
- required this.caption,
- this.onTap,
- }) : status = MetricStatus.offline;
-
- MetricTrend get _trend {
- final variation = variationPercent ?? 0;
- if (variation > 0) return MetricTrend.up;
- if (variation < 0) return MetricTrend.down;
- return MetricTrend.flat;
- }
-
- String _formatRate(double rate) => rate.toStringAsFixed(2).replaceAll('.', ',');
-
- String _formatVariation(double variation) {
- final sign = variation > 0 ? '+' : '';
- return '$sign${variation.toStringAsFixed(2).replaceAll('.', ',')}%';
- }
-
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context).extension()!;
-
- switch (status) {
- case MetricStatus.loading:
- return _LoadingCard(theme: theme);
- case MetricStatus.data:
- return _DataCard(
- theme: theme,
- ratePercent: ratePercent!,
- variationPercent: variationPercent!,
- sparklineData: sparklineData,
- caption: caption,
- trend: _trend,
- formatRate: _formatRate,
- formatVariation: _formatVariation,
- onTap: onTap,
- );
- case MetricStatus.offline:
- return _OfflineCard(
- theme: theme,
- ratePercent: ratePercent,
- variationPercent: variationPercent,
- sparklineData: sparklineData,
- caption: caption,
- trend: _trend,
- formatRate: _formatRate,
- formatVariation: _formatVariation,
- onTap: onTap,
- );
- }
- }
-}
-
-/// Estado de dado disponível — layout original do card.
-class _DataCard extends StatelessWidget {
- final MetricCardTheme theme;
- final double ratePercent;
- final double variationPercent;
- final List sparklineData;
- final String caption;
- final MetricTrend trend;
- final String Function(double) formatRate;
- final String Function(double) formatVariation;
- final VoidCallback? onTap;
-
- const _DataCard({
- required this.theme,
- required this.ratePercent,
- required this.variationPercent,
- required this.sparklineData,
- required this.caption,
- required this.trend,
- required this.formatRate,
- required this.formatVariation,
- this.onTap,
- });
-
- @override
- Widget build(BuildContext context) {
- final Color trendColor = switch (trend) {
- MetricTrend.up => theme.variationPositiveColor,
- MetricTrend.down => theme.variationNegativeColor,
- MetricTrend.flat => theme.variationNeutralColor,
- };
-
- final content = Container(
- key: const Key('metric_card_container'),
- width: double.infinity,
- padding: theme.padding,
- decoration: BoxDecoration(
- color: theme.backgroundColor,
- borderRadius: BorderRadius.circular(theme.borderRadius),
- border: Border.all(color: theme.borderColor),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- Row(
- children: [
- Text('Selic atual', style: theme.labelStyle),
- const SizedBox(width: 8),
- Container(
- key: const Key('metric_card_variation_badge'),
- padding: const EdgeInsets.symmetric(
- horizontal: 8, vertical: 2),
- decoration: BoxDecoration(
- color: trendColor.withValues(alpha: 0.12),
- borderRadius: BorderRadius.circular(6),
- ),
- child: Text(
- formatVariation(variationPercent),
- style: theme.variationTextStyle
- .copyWith(color: trendColor),
- ),
- ),
- ],
- ),
- const SizedBox(height: 4),
- Text.rich(
- TextSpan(
- text: formatRate(ratePercent),
- style: theme.valueStyle,
- children: [
- TextSpan(text: '% a.a.', style: theme.valueUnitStyle),
- ],
- ),
- ),
- ],
- ),
- ),
- SizedBox(
- key: const Key('metric_card_sparkline'),
- width: 72,
- height: 32,
- child: CustomPaint(
- painter: SparklinePainter(
- data: sparklineData,
- color: trendColor,
- strokeWidth: theme.sparklineStrokeWidth,
- ),
- ),
- ),
- ],
- ),
- const SizedBox(height: 10),
- Text(caption, style: theme.captionStyle),
- ],
- ),
- );
-
- if (onTap == null) return content;
- return InkWell(
- onTap: onTap,
- borderRadius: BorderRadius.circular(theme.borderRadius),
- child: content,
- );
- }
-}
-
-/// Estado offline — mostra o último valor em cache (esmaecido) se houver,
-/// ou um estado vazio com "tentar novamente" se não houver nada salvo.
-class _OfflineCard extends StatelessWidget {
- final MetricCardTheme theme;
- final double? ratePercent;
- final double? variationPercent;
- final List sparklineData;
- final String caption;
- final MetricTrend trend;
- final String Function(double) formatRate;
- final String Function(double) formatVariation;
- final VoidCallback? onTap;
-
- const _OfflineCard({
- required this.theme,
- required this.ratePercent,
- required this.variationPercent,
- required this.sparklineData,
- required this.caption,
- required this.trend,
- required this.formatRate,
- required this.formatVariation,
- this.onTap,
- });
-
- bool get _hasCachedData => ratePercent != null;
-
- @override
- Widget build(BuildContext context) {
- final container = Container(
- key: const Key('metric_card_container'),
- width: double.infinity,
- padding: theme.padding,
- decoration: BoxDecoration(
- color: theme.backgroundColor,
- borderRadius: BorderRadius.circular(theme.borderRadius),
- border: Border.all(color: theme.borderColor),
- ),
- child: _hasCachedData ? _cachedContent() : _emptyContent(),
- );
-
- if (onTap == null) return container;
- return InkWell(
- onTap: onTap,
- borderRadius: BorderRadius.circular(theme.borderRadius),
- child: container,
- );
- }
-
- Widget _offlineTag() {
- return Container(
- key: const Key('metric_card_offline_tag'),
- padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
- decoration: BoxDecoration(
- color: theme.variationNeutralColor.withValues(alpha: 0.12),
- borderRadius: BorderRadius.circular(6),
- ),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.wifi_off, size: 11, color: theme.variationNeutralColor),
- const SizedBox(width: 4),
- Text(
- 'offline',
- style:
- theme.variationTextStyle.copyWith(color: theme.variationNeutralColor),
- ),
- ],
- ),
- );
- }
-
- Widget _cachedContent() {
- // Valor em cache, mas visualmente esmaecido — deixa claro que não é
- // o dado mais recente, sem esconder a última informação conhecida.
- return Opacity(
- opacity: 0.6,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- Row(
- children: [
- Text('Selic atual', style: theme.labelStyle),
- const SizedBox(width: 8),
- _offlineTag(),
- ],
- ),
- const SizedBox(height: 4),
- Text.rich(
- TextSpan(
- text: formatRate(ratePercent!),
- style: theme.valueStyle,
- children: [
- TextSpan(text: '% a.a.', style: theme.valueUnitStyle),
- ],
- ),
- ),
- const SizedBox(height: 10),
- Text(caption, style: theme.captionStyle),
- ],
- ),
- );
- }
-
- Widget _emptyContent() {
- return Row(
- children: [
- Icon(Icons.wifi_off, size: 18, color: theme.variationNeutralColor),
- const SizedBox(width: 12),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(caption, style: theme.captionStyle),
- if (onTap != null) ...[
- const SizedBox(height: 4),
- Text(
- 'Toque para tentar novamente',
- style: theme.variationTextStyle
- .copyWith(color: theme.variationNeutralColor),
- ),
- ],
- ],
- ),
- ),
- ],
- );
- }
-}
-
-/// Estado de carregamento — placeholders no formato do layout final,
-/// com uma animação sutil de pulso pra sinalizar "carregando".
-class _LoadingCard extends StatefulWidget {
- final MetricCardTheme theme;
-
- const _LoadingCard({required this.theme});
-
- @override
- State<_LoadingCard> createState() => _LoadingCardState();
-}
-
-class _LoadingCardState extends State<_LoadingCard>
- with SingleTickerProviderStateMixin {
- late final AnimationController _controller;
-
- @override
- void initState() {
- super.initState();
- _controller = AnimationController(
- vsync: this,
- duration: const Duration(milliseconds: 900),
- )..repeat(reverse: true);
- }
-
- @override
- void dispose() {
- _controller.dispose();
- super.dispose();
- }
-
- Widget _bar({required double width, required double height}) {
- return Container(
- width: width,
- height: height,
- decoration: BoxDecoration(
- color: widget.theme.skeletonColor,
- borderRadius: BorderRadius.circular(4),
- ),
- );
- }
-
- @override
- Widget build(BuildContext context) {
- final theme = widget.theme;
-
- return Container(
- key: const Key('metric_card_container'),
- width: double.infinity,
- padding: theme.padding,
- decoration: BoxDecoration(
- color: theme.backgroundColor,
- borderRadius: BorderRadius.circular(theme.borderRadius),
- border: Border.all(color: theme.borderColor),
- ),
- child: FadeTransition(
- opacity: _controller.drive(
- Tween(begin: 0.4, end: 0.85),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- _bar(width: 90, height: 12),
- const SizedBox(height: 10),
- _bar(width: 130, height: 28),
- ],
- ),
- ),
- _bar(width: 72, height: 32),
- ],
- ),
- const SizedBox(height: 12),
- _bar(width: 180, height: 11),
- ],
- ),
- ),
- );
- }
-}
diff --git a/lib/modules/home/presentation/components/selic_band.dart b/lib/modules/home/presentation/components/selic_band.dart
new file mode 100644
index 0000000..ffb937e
--- /dev/null
+++ b/lib/modules/home/presentation/components/selic_band.dart
@@ -0,0 +1,431 @@
+import 'package:flutter/material.dart';
+import 'package:real_calc/core/widgets/app_icon.dart';
+import 'package:intl/intl.dart';
+import 'package:real_calc/core/themes/app_colors.dart';
+import 'package:real_calc/core/themes/spacing.dart';
+import 'package:real_calc/core/themes/text_styles.dart';
+import 'package:real_calc/modules/metrics/domain/entites/metric.dart';
+import 'package:real_calc/modules/metrics/domain/enum/selic_period.dart';
+
+import '../cubit/selic_cubit.dart';
+import 'sparkline_painter.dart';
+
+final _rateFormat = NumberFormat('#,##0.00', 'pt_BR');
+
+class SelicBand extends StatelessWidget {
+ static const chartHeight = 40.0;
+
+ final SelicState state;
+ final VoidCallback onRetry;
+ final ValueChanged onPeriodChanged;
+ final DateTime Function() now;
+
+ const SelicBand({
+ super.key,
+ required this.state,
+ required this.onRetry,
+ required this.onPeriodChanged,
+ this.now = DateTime.now,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final metric = state.metric;
+
+ if (metric == null) {
+ return state is SelicError
+ ? _Unavailable(onRetry: onRetry)
+ : const _Skeleton();
+ }
+
+ return _MetricView(
+ metric: metric,
+ period: state.period,
+ offline: state.offline,
+ chart: !state.chartUnavailable
+ ? _ChartState.ready
+ : state is SelicLoading
+ ? _ChartState.loading
+ : _ChartState.unavailable,
+ today: now(),
+ onPeriodChanged: onPeriodChanged,
+ onRetry: onRetry,
+ );
+ }
+}
+
+enum _ChartState { ready, loading, unavailable }
+
+class _MetricView extends StatelessWidget {
+ final Metric metric;
+ final SelicPeriod period;
+ final bool offline;
+ final _ChartState chart;
+ final DateTime today;
+ final ValueChanged onPeriodChanged;
+ final VoidCallback onRetry;
+
+ const _MetricView({
+ required this.metric,
+ required this.period,
+ required this.offline,
+ required this.chart,
+ required this.today,
+ required this.onPeriodChanged,
+ required this.onRetry,
+ });
+
+ String get _rate => '${_rateFormat.format(metric.anualRate)}%';
+
+ String get _variation {
+ final change = metric.variationPercent.abs();
+ if (change == 0) return 'Sem mudança em relação à anterior';
+ final unit = change < 2 ? 'ponto' : 'pontos';
+ return '${_rateFormat.format(change)} $unit em relação à anterior';
+ }
+
+ String get _updated {
+ final date = metric.updatedAt ?? metric.lastDate;
+ if (date == null || DateUtils.isSameDay(date, today)) {
+ return 'atualizada hoje';
+ }
+ String two(int value) => value.toString().padLeft(2, '0');
+ return 'atualizada em ${two(date.day)}/${two(date.month)}';
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final soft = AppColors.of(context).onBandSoft;
+ final change = metric.variationPercent;
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Wrap(
+ alignment: WrapAlignment.spaceBetween,
+ crossAxisAlignment: WrapCrossAlignment.center,
+ spacing: AppSpacing.md,
+ runSpacing: AppSpacing.xs,
+ children: [
+ Text(
+ 'Taxa Selic hoje',
+ style: AppTextStyles.bandKicker.copyWith(color: Colors.white),
+ ),
+ _PeriodSelector(selected: period, onChanged: onPeriodChanged),
+ ],
+ ),
+ const SizedBox(height: AppSpacing.xs),
+ Opacity(
+ opacity: offline ? 0.5 : 1,
+ child: FittedBox(
+ fit: BoxFit.scaleDown,
+ alignment: Alignment.centerLeft,
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.baseline,
+ textBaseline: TextBaseline.alphabetic,
+ children: [
+ Text(
+ _rate,
+ style: AppTextStyles.bandStatement.copyWith(
+ color: Colors.white,
+ ),
+ ),
+ const SizedBox(width: AppSpacing.sm),
+ Text(
+ 'ao ano',
+ style: AppTextStyles.bandSub.copyWith(color: soft),
+ ),
+ ],
+ ),
+ ),
+ ),
+ Row(
+ children: [
+ if (change != 0) ...[
+ AppIcon(
+ change > 0 ? AppIcons.arrowUp : AppIcons.arrowDown,
+ size: 16,
+ color: Colors.white,
+ ),
+ const SizedBox(width: AppSpacing.xs),
+ ],
+ Expanded(
+ child: Text(
+ _variation,
+ style: AppTextStyles.bandSub.copyWith(color: Colors.white),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: AppSpacing.md),
+ SizedBox(
+ height: SelicBand.chartHeight,
+ width: double.infinity,
+ child: switch (chart) {
+ _ChartState.ready => Semantics(
+ image: true,
+ label:
+ 'Gráfico da Selic nos últimos ${period.label}, '
+ 'valor atual $_rate ao ano',
+ child: CustomPaint(
+ painter: SparklinePainter(
+ data: metric.sparklineData,
+ color: Colors.white.withValues(alpha: offline ? 0.5 : 1),
+ strokeWidth: 1.75,
+ ),
+ ),
+ ),
+ _ChartState.loading => DecoratedBox(
+ decoration: BoxDecoration(
+ color: Colors.white.withValues(alpha: 0.10),
+ borderRadius: BorderRadius.circular(6),
+ ),
+ ),
+ _ChartState.unavailable => Align(
+ alignment: Alignment.centerLeft,
+ child: Text(
+ 'Gráfico de ${period.label} indisponível sem conexão.',
+ style: AppTextStyles.caption.copyWith(color: soft),
+ ),
+ ),
+ },
+ ),
+ const SizedBox(height: AppSpacing.md),
+ if (offline)
+ Wrap(
+ crossAxisAlignment: WrapCrossAlignment.center,
+ spacing: AppSpacing.sm,
+ children: [
+ _OfflineTag(color: soft),
+ Text(
+ 'Último valor salvo, $_updated.',
+ style: AppTextStyles.caption.copyWith(color: soft),
+ ),
+ _Link(label: 'Atualizar', onTap: onRetry),
+ ],
+ )
+ else
+ Text(
+ 'Fonte: Banco Central, $_updated.',
+ style: AppTextStyles.caption.copyWith(color: soft),
+ ),
+ ],
+ );
+ }
+}
+
+class _PeriodSelector extends StatelessWidget {
+ final SelicPeriod selected;
+ final ValueChanged onChanged;
+
+ const _PeriodSelector({required this.selected, required this.onChanged});
+
+ @override
+ Widget build(BuildContext context) {
+ final soft = AppColors.of(context).onBandSoft;
+
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ for (final period in SelicPeriod.values)
+ Semantics(
+ button: true,
+ selected: period == selected,
+ child: InkWell(
+ onTap: () => onChanged(period),
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(minHeight: 44),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 6),
+ child: Center(
+ widthFactor: 1,
+ child: Container(
+ padding: const EdgeInsets.only(bottom: 2),
+ decoration: BoxDecoration(
+ border: Border(
+ bottom: BorderSide(
+ color: period == selected
+ ? Colors.white
+ : Colors.transparent,
+ width: 1.5,
+ ),
+ ),
+ ),
+ child: Text(
+ period.label,
+ style: AppTextStyles.caption.copyWith(
+ fontSize: 13,
+ fontWeight: FontWeight.w700,
+ color: period == selected ? Colors.white : soft,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+class _OfflineTag extends StatelessWidget {
+ final Color color;
+
+ const _OfflineTag({required this.color});
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ AppIcon(AppIcons.wifiOff, size: 14, color: color),
+ const SizedBox(width: 6),
+ Text(
+ 'sem conexão',
+ style: AppTextStyles.caption.copyWith(
+ fontSize: 13,
+ fontWeight: FontWeight.w700,
+ color: color,
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+class _Link extends StatelessWidget {
+ final String label;
+ final VoidCallback onTap;
+
+ const _Link({required this.label, required this.onTap});
+
+ @override
+ Widget build(BuildContext context) {
+ return Semantics(
+ button: true,
+ child: InkWell(
+ onTap: onTap,
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(minHeight: 44),
+ child: Center(
+ widthFactor: 1,
+ child: Text(
+ label,
+ style: AppTextStyles.caption.copyWith(
+ color: Colors.white,
+ fontWeight: FontWeight.w700,
+ decoration: TextDecoration.underline,
+ decorationColor: Colors.white,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _Unavailable extends StatelessWidget {
+ final VoidCallback onRetry;
+
+ const _Unavailable({required this.onRetry});
+
+ @override
+ Widget build(BuildContext context) {
+ final soft = AppColors.of(context).onBandSoft;
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ AppIcon(AppIcons.wifiOff, size: 22, color: soft),
+ const SizedBox(height: AppSpacing.sm),
+ Text(
+ 'Selic indisponível',
+ style: AppTextStyles.screenTitle.copyWith(color: Colors.white),
+ ),
+ const SizedBox(height: AppSpacing.xs),
+ Text(
+ 'Sem conexão com o Banco Central.',
+ style: AppTextStyles.bandSub.copyWith(color: soft),
+ ),
+ _Link(label: 'Tentar de novo', onTap: onRetry),
+ ],
+ );
+ }
+}
+
+class _Skeleton extends StatefulWidget {
+ const _Skeleton();
+
+ @override
+ State<_Skeleton> createState() => _SkeletonState();
+}
+
+class _SkeletonState extends State<_Skeleton>
+ with SingleTickerProviderStateMixin {
+ late final _controller = AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 900),
+ );
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ if (MediaQuery.disableAnimationsOf(context)) {
+ _controller
+ ..stop()
+ ..value = 0;
+ } else if (!_controller.isAnimating) {
+ _controller.repeat(reverse: true);
+ }
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Semantics(
+ label: 'Carregando a taxa Selic',
+ child: AnimatedBuilder(
+ animation: _controller,
+ builder: (context, _) {
+ final color = Colors.white.withValues(
+ alpha: 0.10 + 0.10 * _controller.value,
+ );
+ Widget block(double width, double height) => Container(
+ width: width,
+ height: height,
+ decoration: BoxDecoration(
+ color: color,
+ borderRadius: BorderRadius.circular(6),
+ ),
+ );
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ block(120, 16),
+ const SizedBox(height: 12),
+ block(180, 44),
+ const SizedBox(height: 10),
+ block(220, 14),
+ const SizedBox(height: AppSpacing.md),
+ block(double.infinity, SelicBand.chartHeight),
+ const SizedBox(height: AppSpacing.md),
+ block(200, 12),
+ ],
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/lib/modules/home/presentation/cubit/selic_cubit.dart b/lib/modules/home/presentation/cubit/selic_cubit.dart
index 5dc7829..2a9da77 100644
--- a/lib/modules/home/presentation/cubit/selic_cubit.dart
+++ b/lib/modules/home/presentation/cubit/selic_cubit.dart
@@ -1,25 +1,43 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:real_calc/core/seed_works/error.dart';
import 'package:real_calc/modules/metrics/domain/entites/metric.dart';
+import 'package:real_calc/modules/metrics/domain/enum/selic_period.dart';
import 'package:real_calc/modules/metrics/use_cases/selic/i_selic_use_case.dart';
part 'selic_state.dart';
class SelicCubit extends Cubit {
final ISelicUseCase _useCase;
+ final _metrics = {};
+ var _request = 0;
- SelicCubit(this._useCase) : super(SelicInitial());
+ SelicCubit(this._useCase) : super(const SelicInitial());
- Future load() async {
- final previousMetric = state.metric;
- emit(SelicLoading(previousMetric));
+ Future load() => _fetch(state.period);
- final result = await _useCase.getSelicRate();
- result.fold(
- (error) => emit(SelicError(error, previousMetric)),
- (metric) => emit(SelicLoaded(metric)),
- );
+ Future retry() => load();
+
+ Future changePeriod(SelicPeriod period) async {
+ if (period == state.period) return;
+ await _fetch(period);
}
- Future retry() => load();
+ Future _fetch(SelicPeriod period) async {
+ final request = ++_request;
+ final own = _metrics[period];
+ final shown = own ?? state.metric;
+ final foreign = own == null && shown != null;
+ emit(SelicLoading(shown, period, foreign));
+
+ final result = await _useCase.getSelic(period);
+ // Uma troca de período mais recente já está a caminho.
+ if (isClosed || request != _request) return;
+
+ result.fold((error) => emit(SelicError(error, shown, period, foreign)), (
+ metric,
+ ) {
+ _metrics[period] = metric;
+ emit(SelicLoaded(metric, period));
+ });
+ }
}
diff --git a/lib/modules/home/presentation/cubit/selic_state.dart b/lib/modules/home/presentation/cubit/selic_state.dart
index d672e5e..751433e 100644
--- a/lib/modules/home/presentation/cubit/selic_state.dart
+++ b/lib/modules/home/presentation/cubit/selic_state.dart
@@ -2,8 +2,18 @@ part of 'selic_cubit.dart';
sealed class SelicState {
final Metric? metric;
+ final SelicPeriod period;
- const SelicState([this.metric]);
+ /// `metric` é de outro período: a taxa vale, mas o gráfico não.
+ final bool chartUnavailable;
+
+ const SelicState([
+ this.metric,
+ this.period = SelicPeriod.months12,
+ this.chartUnavailable = false,
+ ]);
+
+ bool get offline => this is SelicError || (metric?.offline ?? false);
}
final class SelicInitial extends SelicState {
@@ -11,15 +21,20 @@ final class SelicInitial extends SelicState {
}
final class SelicLoading extends SelicState {
- const SelicLoading([super.metric]);
+ const SelicLoading([super.metric, super.period, super.chartUnavailable]);
}
final class SelicLoaded extends SelicState {
- const SelicLoaded(super.metric);
+ const SelicLoaded(Metric super.metric, [super.period]);
}
final class SelicError extends SelicState {
final ErrorMessages error;
- const SelicError(this.error, [super.metric]);
-}
\ No newline at end of file
+ const SelicError(
+ this.error, [
+ super.metric,
+ super.period,
+ super.chartUnavailable,
+ ]);
+}
diff --git a/lib/modules/home/presentation/page/page.dart b/lib/modules/home/presentation/page/page.dart
index 13b8ebe..d2f441b 100644
--- a/lib/modules/home/presentation/page/page.dart
+++ b/lib/modules/home/presentation/page/page.dart
@@ -1,139 +1,164 @@
import 'package:flutter/material.dart';
+import 'package:real_calc/core/widgets/app_icon.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:real_calc/core/routes/app_routes.dart';
+import 'package:real_calc/core/themes/app_colors.dart';
+import 'package:real_calc/core/themes/calculator_palette.dart';
import 'package:real_calc/core/themes/spacing.dart';
-import 'package:real_calc/core/widgets/title_widget.dart';
-import 'package:real_calc/modules/home/domain/enum/menu_card_variant.dart';
-import '../../../../core/themes/extensions/home_page_theme.dart';
-import '../components/menu_card.dart';
-import '../components/metric_card.dart';
+import 'package:real_calc/core/themes/text_styles.dart';
+import 'package:real_calc/core/widgets/app_logo.dart';
+import 'package:real_calc/core/widgets/calculator_band/calculator_band.dart';
+import 'package:real_calc/core/widgets/theme_toggle_button.dart';
+
+import '../components/calculator_list_item.dart';
+import '../components/selic_band.dart';
import '../cubit/greeting_cubit.dart';
import '../cubit/selic_cubit.dart';
class HomePage extends StatelessWidget {
- const HomePage({super.key});
-
- BlocBuilder _buildGreetingWidget(
- BuildContext context,
- ) {
- return BlocBuilder(
- builder: (context, greeting) =>
- TitleWidget(title: greeting, subtitle: 'O que vamos calcular?', invert: true),
- );
- }
+ static const footer =
+ 'Mesma metodologia da Calculadora do Cidadão, do Banco Central.';
- BlocBuilder _buildMetricCard(BuildContext context) {
- return BlocBuilder(
- builder: (context, state) {
- if (state is SelicLoading || state is SelicInitial) {
- return const MetricCard.loading();
- }
+ static const calculators = [
+ (
+ kind: CalculatorKind.financing,
+ icon: AppIcons.bank,
+ title: 'Financiamento',
+ description: 'Prestação, prazo ou taxa',
+ route: AppRoutes.financing,
+ ),
+ (
+ kind: CalculatorKind.deposits,
+ icon: AppIcons.piggyBank,
+ title: 'Depósitos Regulares',
+ description: 'Quanto você acumula todo mês',
+ route: AppRoutes.deposits,
+ ),
+ (
+ kind: CalculatorKind.futureValue,
+ icon: AppIcons.trendingUp,
+ title: 'Valor Futuro',
+ description: 'Quanto rende um valor aplicado',
+ route: AppRoutes.futureValue,
+ ),
+ (
+ kind: CalculatorKind.correction,
+ icon: AppIcons.cycle,
+ title: 'Correção de Valores',
+ description: 'Pela inflação, juros ou poupança',
+ route: AppRoutes.correction,
+ ),
+ ];
- final metric = state.metric;
- if (state is SelicLoaded && metric != null) {
- return MetricCard.data(
- ratePercent: metric.anualRate,
- variationPercent: metric.variationPercent,
- sparklineData: metric.sparklineData,
- caption: 'Atualizada pelo Banco Central',
- onTap: BlocProvider.of(context).retry,
- );
- }
+ const HomePage({super.key});
- return MetricCard.offline(
- ratePercent: metric?.anualRate,
- variationPercent: metric?.variationPercent,
- sparklineData: metric?.sparklineData ?? const [],
- caption: state is SelicError
- ? state.error.message
- : 'Não foi possível carregar a taxa Selic',
- onTap: BlocProvider.of(context).retry,
- );
- },
- );
- }
+ @override
+ Widget build(BuildContext context) {
+ final colors = AppColors.of(context);
- Widget _buildOpitionForms(BuildContext context, HomePageTheme theme) {
- return Column(
- spacing: AppSpacing.md,
- children: [
- MenuCard(
- icon: Icons.account_balance,
- title: 'Financiamento',
- description: 'Prestações com juros compostos',
- cardColor: theme.cardColor,
- iconColor: theme.iconContainerColor,
- onTap: () => Modular.to.pushNamed(AppRoutes.financing),
- variant: MenuCardVariant.featured,
- ),
- SizedBox(
- height: 100,
- child: Row(
- children: [
- Expanded(
- child: MenuCard(
- icon: Icons.account_balance,
- title: 'Depósitos Regulares',
- description: 'Prestações com juros compostos',
- cardColor: theme.cardColor,
- iconColor: theme.iconContainerColor,
- onTap: () => Modular.to.pushNamed(AppRoutes.deposits),
- variant: MenuCardVariant.standard,
+ return Scaffold(
+ body: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ AnnotatedRegion(
+ value: CalculatorBand.statusBarStyle,
+ child: ColoredBox(
+ color: colors.bandHome,
+ child: SafeArea(
+ bottom: false,
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(
+ AppSpacing.screen,
+ AppSpacing.sm,
+ AppSpacing.screen - 10,
+ AppSpacing.lg,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ const Row(
+ children: [AppLogo(), Spacer(), ThemeToggleButton()],
+ ),
+ const SizedBox(height: AppSpacing.md),
+ Padding(
+ padding: const EdgeInsets.only(right: 10),
+ child: BlocBuilder(
+ builder: (context, state) => SelicBand(
+ state: state,
+ onRetry: BlocProvider.of(context).retry,
+ onPeriodChanged: BlocProvider.of(
+ context,
+ ).changePeriod,
+ ),
+ ),
+ ),
+ ],
+ ),
),
),
- VerticalDivider(
- color: theme.lineColor,
- thickness: 1,
- width: AppSpacing.md,
+ ),
+ ),
+ Expanded(
+ child: SingleChildScrollView(
+ padding: const EdgeInsets.fromLTRB(
+ AppSpacing.screen,
+ AppSpacing.lg,
+ AppSpacing.screen,
+ AppSpacing.lg,
),
- Expanded(
- child: MenuCard(
- icon: Icons.bar_chart,
- title: 'Valor Futuro',
- description: 'Prestações com juros compostos',
- cardColor: theme.cardColor,
- iconColor: theme.iconContainerColor,
- onTap: () => Modular.to.pushNamed(AppRoutes.futureValue),
- variant: MenuCardVariant.standard,
- ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ BlocBuilder(
+ builder: (context, greeting) => Text(
+ greeting,
+ style: AppTextStyles.body.copyWith(
+ color: colors.textSecondary,
+ ),
+ ),
+ ),
+ Semantics(
+ header: true,
+ child: Text(
+ 'O que vamos calcular hoje?',
+ style: AppTextStyles.screenTitle.copyWith(
+ color: colors.textPrimary,
+ ),
+ ),
+ ),
+ const SizedBox(height: AppSpacing.md),
+ DecoratedBox(
+ decoration: BoxDecoration(
+ border: Border(top: BorderSide(color: colors.hairline)),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ for (final calculator in calculators)
+ CalculatorListItem(
+ kind: calculator.kind,
+ iconAsset: calculator.icon,
+ title: calculator.title,
+ description: calculator.description,
+ onTap: () => Modular.to.pushNamed(calculator.route),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: AppSpacing.lg),
+ Text(
+ footer,
+ style: AppTextStyles.caption.copyWith(
+ color: colors.textHint,
+ ),
+ ),
+ ],
),
- ],
- ),
- ),
- MenuCard(
- icon: Icons.trending_up,
- title: 'Correção de Valores',
- description: 'Atualize valores por índices de inflação',
- cardColor: theme.cardColor,
- iconColor: theme.iconContainerColor,
- onTap: () => Modular.to.pushNamed(AppRoutes.correction),
- variant: MenuCardVariant.standard,
- ),
- ],
- );
- }
-
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context).extension()!;
- final spacing = 4;
-
- return Scaffold(
- body: SafeArea(
- child: SingleChildScrollView(
- padding: const EdgeInsets.all(AppSpacing.lg),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- spacing: AppSpacing.xl,
- children: [
- SizedBox(height: AppSpacing.xs- spacing),
- _buildGreetingWidget(context),
- _buildMetricCard(context),
- _buildOpitionForms(context, theme),
- ],
+ ),
),
- ),
+ ],
),
);
}
diff --git a/lib/modules/metrics/domain/entites/metric.dart b/lib/modules/metrics/domain/entites/metric.dart
index 8681c61..34f6288 100644
--- a/lib/modules/metrics/domain/entites/metric.dart
+++ b/lib/modules/metrics/domain/entites/metric.dart
@@ -1,13 +1,37 @@
import 'package:real_calc/core/seed_works/entity.dart';
-class Metric extends Entity{
+class Metric extends Entity {
final double anualRate;
final double variationPercent;
final List sparklineData;
+ final List sparklineDates;
+ final DateTime? lastDate;
+ final bool offline;
Metric({
- required this.anualRate,
- required this.variationPercent,
- required this.sparklineData
+ required this.anualRate,
+ required this.variationPercent,
+ required this.sparklineData,
+ this.sparklineDates = const [],
+ this.lastDate,
+ this.offline = false,
+ super.updatedAt,
});
-}
\ No newline at end of file
+
+ Metric copyWith({
+ List? sparklineData,
+ List? sparklineDates,
+ bool? offline,
+ DateTime? updatedAt,
+ }) {
+ return Metric(
+ anualRate: anualRate,
+ variationPercent: variationPercent,
+ sparklineData: sparklineData ?? this.sparklineData,
+ sparklineDates: sparklineDates ?? this.sparklineDates,
+ lastDate: lastDate,
+ offline: offline ?? this.offline,
+ updatedAt: updatedAt ?? this.updatedAt,
+ );
+ }
+}
diff --git a/lib/modules/metrics/domain/enum/selic_period.dart b/lib/modules/metrics/domain/enum/selic_period.dart
new file mode 100644
index 0000000..776d393
--- /dev/null
+++ b/lib/modules/metrics/domain/enum/selic_period.dart
@@ -0,0 +1,9 @@
+enum SelicPeriod {
+ months12('12 meses', 12),
+ years5('5 anos', 60);
+
+ final String label;
+ final int months;
+
+ const SelicPeriod(this.label, this.months);
+}
diff --git a/lib/modules/metrics/domain/repositories/selic/i_selic_cache_repository.dart b/lib/modules/metrics/domain/repositories/selic/i_selic_cache_repository.dart
new file mode 100644
index 0000000..8f97d36
--- /dev/null
+++ b/lib/modules/metrics/domain/repositories/selic/i_selic_cache_repository.dart
@@ -0,0 +1,7 @@
+import '../../entites/metric.dart';
+import '../../enum/selic_period.dart';
+
+abstract class ISelicCacheRepository {
+ Future read(SelicPeriod period);
+ Future save(SelicPeriod period, Metric metric);
+}
diff --git a/lib/modules/metrics/infrastructure/repositories/selic_cache_repository.dart b/lib/modules/metrics/infrastructure/repositories/selic_cache_repository.dart
new file mode 100644
index 0000000..ba147bb
--- /dev/null
+++ b/lib/modules/metrics/infrastructure/repositories/selic_cache_repository.dart
@@ -0,0 +1,65 @@
+import 'dart:convert';
+
+import 'package:shared_preferences/shared_preferences.dart';
+
+import '../../domain/entites/metric.dart';
+import '../../domain/enum/selic_period.dart';
+import '../../domain/repositories/selic/i_selic_cache_repository.dart';
+
+class SelicCacheRepository implements ISelicCacheRepository {
+ static String keyFor(SelicPeriod period) => 'selic_cache_${period.name}';
+
+ final Future Function() _preferences;
+
+ SelicCacheRepository([this._preferences = SharedPreferences.getInstance]);
+
+ @override
+ Future read(SelicPeriod period) async {
+ final raw = (await _preferences()).getString(keyFor(period));
+ if (raw == null) return null;
+ try {
+ return _fromJson(jsonDecode(raw) as Map);
+ } on Object {
+ return null;
+ }
+ }
+
+ @override
+ Future save(SelicPeriod period, Metric metric) async {
+ await (await _preferences()).setString(
+ keyFor(period),
+ jsonEncode(_toJson(metric)),
+ );
+ }
+
+ Map _toJson(Metric metric) => {
+ 'anualRate': metric.anualRate,
+ 'variationPercent': metric.variationPercent,
+ 'sparklineData': metric.sparklineData,
+ 'sparklineDates': [
+ for (final date in metric.sparklineDates) date.toIso8601String(),
+ ],
+ 'lastDate': metric.lastDate?.toIso8601String(),
+ 'updatedAt': metric.updatedAt?.toIso8601String(),
+ };
+
+ Metric _fromJson(Map json) {
+ DateTime? date(Object? value) =>
+ value is String ? DateTime.parse(value) : null;
+
+ return Metric(
+ anualRate: (json['anualRate'] as num).toDouble(),
+ variationPercent: (json['variationPercent'] as num).toDouble(),
+ sparklineData: [
+ for (final value in json['sparklineData'] as List)
+ (value as num).toDouble(),
+ ],
+ sparklineDates: [
+ for (final value in json['sparklineDates'] as List)
+ DateTime.parse(value as String),
+ ],
+ lastDate: date(json['lastDate']),
+ updatedAt: date(json['updatedAt']),
+ );
+ }
+}
diff --git a/lib/modules/metrics/infrastructure/repositories/selic_repository.dart b/lib/modules/metrics/infrastructure/repositories/selic_repository.dart
index 751eb80..544edb8 100644
--- a/lib/modules/metrics/infrastructure/repositories/selic_repository.dart
+++ b/lib/modules/metrics/infrastructure/repositories/selic_repository.dart
@@ -11,10 +11,11 @@ class SelicRepository implements ISelicNetworkOnlyRepository {
SelicRepository(this._selicDatasource);
@override
- Future>> getSelicRate() async {
+ Future>>
+ getSelicRate() async {
final dataFinal = DateTime.now();
final dataInicial = DateTime(
- dataFinal.year - 4,
+ dataFinal.year - 1,
dataFinal.month,
dataFinal.day,
);
@@ -24,7 +25,8 @@ class SelicRepository implements ISelicNetworkOnlyRepository {
);
return response.fold(
- (error) => FailureResult>(error),
+ (error) =>
+ FailureResult>(error),
(data) => SuccessResult>(
data
.whereType