kasy-cli 1.16.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/bin/kasy.js +16 -2
  2. package/lib/commands/add.js +52 -19
  3. package/lib/commands/configure.js +548 -0
  4. package/lib/commands/deploy.js +4 -4
  5. package/lib/commands/doctor.js +54 -6
  6. package/lib/commands/favicon.js +4 -4
  7. package/lib/commands/icon.js +5 -5
  8. package/lib/commands/new.js +404 -213
  9. package/lib/commands/remove.js +14 -3
  10. package/lib/commands/run.js +208 -6
  11. package/lib/commands/splash.js +5 -5
  12. package/lib/commands/update.js +9 -9
  13. package/lib/scaffold/CHANGELOG.json +23 -0
  14. package/lib/scaffold/backends/api/patch/README.md +3 -2
  15. package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +108 -0
  16. package/lib/scaffold/backends/firebase/setup-from-scratch.js +44 -5
  17. package/lib/scaffold/backends/supabase/patch/README.md +3 -2
  18. package/lib/scaffold/generate.js +24 -8
  19. package/lib/scaffold/shared/generator-utils.js +52 -8
  20. package/lib/scaffold/shared/post-build.js +113 -31
  21. package/lib/scaffold/shared/template-strings.js +6 -0
  22. package/lib/utils/brand.js +16 -12
  23. package/lib/utils/flutter-run.js +139 -11
  24. package/lib/utils/i18n/messages-en.js +85 -7
  25. package/lib/utils/i18n/messages-es.js +85 -7
  26. package/lib/utils/i18n/messages-pt.js +86 -8
  27. package/lib/utils/ui.js +79 -4
  28. package/package.json +1 -1
  29. package/templates/firebase/README.en.md +18 -8
  30. package/templates/firebase/README.es.md +18 -8
  31. package/templates/firebase/README.md +18 -8
  32. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +68 -45
  33. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidgetReceiver.kt +37 -0
  34. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/OpenAppAction.kt +26 -0
  35. package/templates/firebase/android/app/src/main/res/drawable/widget_add_button.xml +2 -2
  36. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +7 -2
  37. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +6 -6
  38. package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +1 -1
  39. package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +2 -2
  40. package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +1 -1
  41. package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
  42. package/templates/firebase/android/app/src/main/res/drawable-hdpi/splash.png +0 -0
  43. package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
  44. package/templates/firebase/android/app/src/main/res/drawable-mdpi/splash.png +0 -0
  45. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
  46. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/splash.png +0 -0
  47. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
  48. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/splash.png +0 -0
  49. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
  50. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/splash.png +0 -0
  51. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
  52. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/splash.png +0 -0
  53. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
  54. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/splash.png +0 -0
  55. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
  56. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/splash.png +0 -0
  57. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
  58. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/splash.png +0 -0
  59. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
  60. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/splash.png +0 -0
  61. package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +3 -3
  62. package/templates/firebase/android/app/src/main/res/values/colors.xml +32 -0
  63. package/templates/firebase/assets/images/splash_logo_dark.png +0 -0
  64. package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
  65. package/templates/firebase/assets/images/splash_logo_light.png +0 -0
  66. package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
  67. package/templates/firebase/docs/revenuecat-setup.es.md +28 -8
  68. package/templates/firebase/docs/revenuecat-setup.pt.md +28 -8
  69. package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +75 -29
  70. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
  71. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
  72. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
  73. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png +0 -0
  74. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png +0 -0
  75. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png +0 -0
  76. package/templates/firebase/ios/Runner/Base.lproj/LaunchScreen.storyboard +1 -1
  77. package/templates/firebase/lib/components/components.dart +1 -0
  78. package/templates/firebase/lib/components/kasy_avatar.dart +65 -17
  79. package/templates/firebase/lib/components/kasy_avatar_presets.dart +121 -97
  80. package/templates/firebase/lib/components/kasy_button.dart +8 -8
  81. package/templates/firebase/lib/components/kasy_date_picker.dart +834 -0
  82. package/templates/firebase/lib/components/kasy_tabs.dart +145 -61
  83. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +45 -53
  84. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +565 -77
  85. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +10 -5
  86. package/templates/firebase/lib/i18n/en.i18n.json +2 -1
  87. package/templates/firebase/lib/i18n/es.i18n.json +2 -1
  88. package/templates/firebase/lib/i18n/pt.i18n.json +2 -1
  89. package/templates/firebase/lib/router.dart +15 -1
  90. package/templates/firebase/pubspec.yaml +1 -1
  91. package/templates/firebase/web/index.html +9 -0
  92. package/templates/firebase/web/splash/img/dark-1x.png +0 -0
  93. package/templates/firebase/web/splash/img/dark-2x.png +0 -0
  94. package/templates/firebase/web/splash/img/dark-3x.png +0 -0
  95. package/templates/firebase/web/splash/img/dark-4x.png +0 -0
  96. package/templates/firebase/web/splash/img/light-1x.png +0 -0
  97. package/templates/firebase/web/splash/img/light-2x.png +0 -0
  98. package/templates/firebase/web/splash/img/light-3x.png +0 -0
  99. package/templates/firebase/web/splash/img/light-4x.png +0 -0
@@ -17,7 +17,7 @@
17
17
  android:layout_width="wrap_content"
18
18
  android:layout_height="wrap_content"
19
19
  android:text="Boa noite"
20
- android:textColor="#8CFFFFFF"
20
+ android:textColor="@color/widget_text_subtle"
21
21
  android:textSize="11sp" />
22
22
 
23
23
  <TextView
@@ -25,7 +25,7 @@
25
25
  android:layout_height="wrap_content"
26
26
  android:layout_marginTop="4dp"
27
27
  android:text="Olá!"
28
- android:textColor="#FFFFFFFF"
28
+ android:textColor="@color/widget_text_strong"
29
29
  android:textSize="22sp"
30
30
  android:textStyle="bold" />
31
31
 
@@ -46,7 +46,7 @@
46
46
  android:paddingTop="5dp"
47
47
  android:paddingBottom="5dp"
48
48
  android:text="⭐ PRO"
49
- android:textColor="#FFFFD700"
49
+ android:textColor="@color/widget_pro_gold"
50
50
  android:textSize="11sp"
51
51
  android:textStyle="bold" />
52
52
 
@@ -0,0 +1,32 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!--
3
+ Brand colors for the MyWidget home widget — single source of truth on Android.
4
+
5
+ To rebrand the widget, edit ONLY this file. Every Android XML asset under
6
+ res/drawable/widget_* and res/layout/widget_* references these names instead
7
+ of inlining hex values.
8
+
9
+ Keep in sync with iOS counterparts in:
10
+ Firebase/ios/HomeWidgetExtension/MyWidget.swift (enum WidgetBrand)
11
+ -->
12
+ <resources>
13
+ <!-- Background gradient used behind every widget surface. -->
14
+ <color name="widget_gradient_start">#FF140829</color>
15
+ <color name="widget_gradient_end">#FF33176B</color>
16
+
17
+ <!-- PRO plan pill (gold). _bg is the soft fill behind the gold text. -->
18
+ <color name="widget_pro_gold">#FFFFD700</color>
19
+ <color name="widget_pro_pill_bg">#2EFFD700</color>
20
+
21
+ <!-- Free plan pill — low-emphasis translucent white. -->
22
+ <color name="widget_free_pill_bg">#14FFFFFF</color>
23
+
24
+ <!-- "+" circular button on medium/large widgets. -->
25
+ <color name="widget_add_button_bg">#2EFFFFFF</color>
26
+ <color name="widget_add_button_stroke">#FFFFFFFF</color>
27
+
28
+ <!-- Text colors used in the preview layout (gallery only). The Glance
29
+ runtime applies these via Compose, not XML. -->
30
+ <color name="widget_text_subtle">#8CFFFFFF</color>
31
+ <color name="widget_text_strong">#FFFFFFFF</color>
32
+ </resources>
@@ -9,12 +9,24 @@ Guía para activar suscripciones y compras en la app después de que la CLI gene
9
9
  | Ya listo | Lo que falta |
10
10
  |----------|--------------|
11
11
  | Instaló `purchases_flutter` | Crear cuenta en RevenueCat |
12
- | Configuró las claves en `Makefile` y `.vscode/launch.json` | Crear Productos, Entitlements y Offerings en el panel RC |
12
+ | Configuró las claves en `.env` (test/iOS prod/Android prod) | Crear Productos, Entitlements y Offerings en el panel RC |
13
13
  | Generó el código del paywall y del repositorio de suscripciones | Registrar la URL del webhook en el panel RC |
14
14
  | Firebase: desplegó la Cloud Function del webhook | — |
15
15
  | Supabase: desplegó la Edge Function del webhook | — |
16
16
 
17
- > Las claves quedan en `.vscode/launch.json` y `Makefile`. Ambos archivos están en `.gitignore` — las claves nunca van al repositorio.
17
+ > Las claves quedan en `.env` en la raíz (fuente de verdad) y reflejadas en `.vscode/launch.json` + `Makefile`. Todos en `.gitignore` — nunca van al repositorio.
18
+
19
+ ### ¿Qué clave usar?
20
+
21
+ La CLI pregunta **tres claves opcionales** (al menos una es obligatoria):
22
+
23
+ | Variable | Prefijo | Uso |
24
+ |---|---|---|
25
+ | `RC_TEST_KEY` | `test_` | Test Store. **Una sola clave**, sirve para iOS+Android. Usada automáticamente en simulador/emulador. |
26
+ | `RC_IOS_PROD_KEY` | `appl_` | App Store (Sandbox + Producción). Usada automáticamente en iPhone físico. |
27
+ | `RC_ANDROID_PROD_KEY` | `goog_` | Google Play (Sandbox + Producción). Usada automáticamente en Android físico. |
28
+
29
+ `kasy run` elige la clave correcta según el dispositivo. Forzar manualmente: `kasy run --rc=test` o `kasy run --rc=prod`.
18
30
 
19
31
  ---
20
32
 
@@ -155,9 +167,13 @@ Después de completar todo y configurar el idioma del grupo, el estado sale de *
155
167
 
156
168
  [app.revenuecat.com](https://app.revenuecat.com) → tu proyecto → **Apps** → `+ Add app` → **App Store** → copia la clave `appl_xxx`.
157
169
 
158
- Actualiza en los archivos del proyecto:
159
- - `.vscode/launch.json` → reemplaza `RC_IOS_API_KEY`
160
- - `Makefile` → reemplaza la misma variable en el bloque `DEFINES`
170
+ Pégala en el `.env` de la raíz:
171
+
172
+ ```env
173
+ RC_IOS_PROD_KEY=appl_xxxxxxxxxxxxxxx
174
+ ```
175
+
176
+ `kasy run` usa esta clave automáticamente en iPhone físico (el simulador sigue con `RC_TEST_KEY`).
161
177
 
162
178
  **6. Crear Sandbox Tester — obligatorio para probar en iPhone físico**
163
179
 
@@ -253,9 +269,13 @@ d) Google Play Console → **Configuración** → **Usuarios y permisos** → **
253
269
 
254
270
  [app.revenuecat.com](https://app.revenuecat.com) → tu proyecto → **Apps** → `+ Add app` → **Google Play** → sube el archivo JSON → copia la clave `goog_xxx`.
255
271
 
256
- Actualiza en los archivos del proyecto:
257
- - `.vscode/launch.json` → reemplaza `RC_ANDROID_API_KEY`
258
- - `Makefile` → reemplaza la misma variable en el bloque `DEFINES`
272
+ Pégala en el `.env` de la raíz:
273
+
274
+ ```env
275
+ RC_ANDROID_PROD_KEY=goog_xxxxxxxxxxxxxxx
276
+ ```
277
+
278
+ `kasy run` usa esta clave automáticamente en Android físico (el emulador sigue con `RC_TEST_KEY`).
259
279
 
260
280
  **8. Agregar License Tester — obligatorio para probar en el dispositivo**
261
281
 
@@ -9,12 +9,24 @@ Guia para ativar assinaturas e compras no app depois que a CLI gerou o projeto.
9
9
  | Já pronto | O que ainda falta |
10
10
  |-----------|-------------------|
11
11
  | Instalou `purchases_flutter` | Criar conta no RevenueCat |
12
- | Configurou as chaves no `Makefile` e `.vscode/launch.json` | Criar Produtos, Entitlements e Offerings no painel RC |
12
+ | Configurou as chaves no `.env` (test/iOS prod/Android prod) | Criar Produtos, Entitlements e Offerings no painel RC |
13
13
  | Gerou o código do paywall e do repositório de assinaturas | Registrar a URL do webhook no painel RC |
14
14
  | Firebase: implantou a Cloud Function do webhook | — |
15
15
  | Supabase: implantou a Edge Function do webhook | — |
16
16
 
17
- > As chaves ficam em `.vscode/launch.json` e `Makefile`. Esses dois arquivos estão no `.gitignore` — as chaves nunca vão para o repositório.
17
+ > As chaves ficam em `.env` na raiz (fonte da verdade) e refletidas no `.vscode/launch.json` + `Makefile`. Todos no `.gitignore` — nunca vão para o repositório.
18
+
19
+ ### Qual chave usar?
20
+
21
+ A CLI pergunta **três chaves opcionais** (pelo menos uma é obrigatória):
22
+
23
+ | Variável | Prefixo | Uso |
24
+ |---|---|---|
25
+ | `RC_TEST_KEY` | `test_` | Test Store. **Uma chave única**, vale iOS+Android. Usada automaticamente em simulador/emulador. |
26
+ | `RC_IOS_PROD_KEY` | `appl_` | App Store (Sandbox + Produção). Usada automaticamente em iPhone físico. |
27
+ | `RC_ANDROID_PROD_KEY` | `goog_` | Google Play (Sandbox + Produção). Usada automaticamente em Android físico. |
28
+
29
+ O `kasy run` escolhe a chave certa baseado no device. Forçar manual: `kasy run --rc=test` ou `kasy run --rc=prod`.
18
30
 
19
31
  ---
20
32
 
@@ -155,9 +167,13 @@ Após preencher tudo e configurar o idioma do grupo, o status sai de **Missing M
155
167
 
156
168
  [app.revenuecat.com](https://app.revenuecat.com) → seu projeto → **Apps** → `+ Add app` → **App Store** → copie a chave `appl_xxx`.
157
169
 
158
- Atualize nos arquivos do projeto:
159
- - `.vscode/launch.json` → substitua `RC_IOS_API_KEY`
160
- - `Makefile` → substitua a mesma variável no bloco `DEFINES`
170
+ Cole no `.env` da raiz:
171
+
172
+ ```env
173
+ RC_IOS_PROD_KEY=appl_xxxxxxxxxxxxxxx
174
+ ```
175
+
176
+ O `kasy run` usa essa chave automaticamente em iPhone físico (simulador continua com `RC_TEST_KEY`).
161
177
 
162
178
  **6. Criar Sandbox Tester — obrigatório para testar no iPhone físico**
163
179
 
@@ -253,9 +269,13 @@ d) Google Play Console → **Configurações** → **Usuários e permissões**
253
269
 
254
270
  [app.revenuecat.com](https://app.revenuecat.com) → seu projeto → **Apps** → `+ Add app` → **Google Play** → faça upload do arquivo JSON → copie a chave `goog_xxx`.
255
271
 
256
- Atualize nos arquivos do projeto:
257
- - `.vscode/launch.json` → substitua `RC_ANDROID_API_KEY`
258
- - `Makefile` → substitua a mesma variável no bloco `DEFINES`
272
+ Cole no `.env` da raiz:
273
+
274
+ ```env
275
+ RC_ANDROID_PROD_KEY=goog_xxxxxxxxxxxxxxx
276
+ ```
277
+
278
+ O `kasy run` usa essa chave automaticamente em Android físico (emulador continua com `RC_TEST_KEY`).
259
279
 
260
280
  **8. Adicionar License Tester — obrigatório para testar no dispositivo**
261
281
 
@@ -1,6 +1,29 @@
1
1
  import WidgetKit
2
2
  import SwiftUI
3
3
 
4
+ // MARK: - Brand
5
+ //
6
+ // Single source of truth for widget colors on iOS. To rebrand the widget,
7
+ // edit ONLY this enum.
8
+ //
9
+ // IMPORTANT: keep these values in sync with the Android counterparts in
10
+ // Firebase/android/app/src/main/res/values/colors.xml.
11
+ enum WidgetBrand {
12
+ // Background gradient.
13
+ static let gradientStart = Color(red: 0.08, green: 0.03, blue: 0.16) // #FF140829
14
+ static let gradientEnd = Color(red: 0.20, green: 0.09, blue: 0.42) // #FF33176B
15
+
16
+ // PRO plan pill (gold) + its soft background.
17
+ static let proGold = Color(red: 1.00, green: 0.84, blue: 0.00) // #FFFFD700
18
+ static let proPillBg = Color(red: 1.00, green: 0.84, blue: 0.00).opacity(0.18)
19
+
20
+ // Free plan pill — low-emphasis translucent white.
21
+ static let freePillBg = Color.white.opacity(0.08)
22
+
23
+ // "+" circular button on medium/large widgets.
24
+ static let addButtonBg = Color.white.opacity(0.18)
25
+ }
26
+
4
27
  struct MyWidgetProvider: TimelineProvider {
5
28
  func placeholder(in context: Context) -> MyWidgetEntry {
6
29
  MyWidgetEntry.defaults()
@@ -24,6 +47,7 @@ struct MyWidgetEntry: TimelineEntry {
24
47
  let planText: String
25
48
  let isPro: Bool
26
49
  let quote: String
50
+ let quoteAuthor: String
27
51
 
28
52
  /// Reads the latest data from the shared app group. If a string was never
29
53
  /// written (first install before the Flutter app pushed data), falls back
@@ -36,6 +60,7 @@ struct MyWidgetEntry: TimelineEntry {
36
60
  let storedPlan = prefs?.string(forKey: "planText") ?? ""
37
61
  let storedIsPro = prefs?.string(forKey: "isPro") == "true"
38
62
  let storedQuote = prefs?.string(forKey: "quote") ?? ""
63
+ let storedQuoteAuthor = prefs?.string(forKey: "quoteAuthor") ?? ""
39
64
 
40
65
  let defaults = MyWidgetEntry.defaults()
41
66
  return MyWidgetEntry(
@@ -44,34 +69,33 @@ struct MyWidgetEntry: TimelineEntry {
44
69
  title: storedTitle.isEmpty ? defaults.title : storedTitle,
45
70
  planText: storedPlan,
46
71
  isPro: storedIsPro,
47
- quote: storedQuote
72
+ quote: storedQuote,
73
+ quoteAuthor: storedQuoteAuthor
48
74
  )
49
75
  }
50
76
 
51
- /// Time-aware fallback used when the Flutter side has not yet written
52
- /// data. Picks the language from the device locale so the first render
53
- /// is at least in the user's language; once the app runs, the real
54
- /// values (in the app locale) overwrite this.
77
+ /// Fallback used ONLY in the brief window between the widget being placed
78
+ /// and the Flutter app pushing real values. Kept dead simple — the
79
+ /// time-aware greeting in three languages lives on the Dart side
80
+ /// (home_widget_mywidget_service.dart::_greeting). Duplicating that logic
81
+ /// here was a maintenance trap when adding new locales.
55
82
  static func defaults() -> MyWidgetEntry {
56
- let hour = Calendar.current.component(.hour, from: Date())
57
83
  let lang = Locale.current.language.languageCode?.identifier ?? "en"
58
- let (morning, afternoon, evening, hello): (String, String, String, String)
84
+ let greeting: String
85
+ let hello: String
59
86
  switch lang {
60
- case "pt": (morning, afternoon, evening, hello) = ("Bom dia", "Boa tarde", "Boa noite", "Olá!")
61
- case "es": (morning, afternoon, evening, hello) = ("Buenos días", "Buenas tardes", "Buenas noches", "¡Hola!")
62
- default: (morning, afternoon, evening, hello) = ("Good morning", "Good afternoon", "Good evening", "Hi there!")
87
+ case "pt": (greeting, hello) = ("Olá", "Bem-vindo!")
88
+ case "es": (greeting, hello) = ("Hola", "¡Bienvenido!")
89
+ default: (greeting, hello) = ("Hello", "Welcome!")
63
90
  }
64
- let greeting: String
65
- if hour < 12 { greeting = morning }
66
- else if hour < 18 { greeting = afternoon }
67
- else { greeting = evening }
68
91
  return MyWidgetEntry(
69
92
  date: Date(),
70
93
  greeting: greeting,
71
94
  title: hello,
72
95
  planText: "",
73
96
  isPro: false,
74
- quote: ""
97
+ quote: "",
98
+ quoteAuthor: ""
75
99
  )
76
100
  }
77
101
  }
@@ -107,19 +131,41 @@ struct MyWidgetWidgetView: View {
107
131
  .frame(maxWidth: .infinity, alignment: .leading)
108
132
  .padding(.trailing, 8)
109
133
 
110
- // Motivational quote only on the large widget small/medium
111
- // don't have the vertical room. Thin, low-emphasis typography
112
- // so the title stays the hero element.
113
- if family == .systemLarge && !entry.quote.isEmpty {
134
+ // Quote line counts per iOS widget family (fixed sizes, so we
135
+ // can hand-tune instead of measuring):
136
+ // small → 1 line (just the first sentence, no attribution)
137
+ // medium 2 lines (horizontal, short fits two short lines)
138
+ // large → 4 lines + bold attribution (the whole quote)
139
+ if !entry.quote.isEmpty {
140
+ let lineLimit: Int = {
141
+ switch family {
142
+ case .systemSmall: return 1
143
+ case .systemMedium: return 2
144
+ default: return 4
145
+ }
146
+ }()
147
+
114
148
  Spacer().frame(height: 12)
115
149
  Text(entry.quote)
116
150
  .font(.system(size: 15, weight: .light, design: .rounded))
117
151
  .italic()
118
152
  .foregroundStyle(.white.opacity(0.7))
119
- .lineLimit(4)
153
+ .lineLimit(lineLimit)
120
154
  .lineSpacing(2)
121
155
  .frame(maxWidth: .infinity, alignment: .leading)
122
156
  .padding(.trailing, 8)
157
+
158
+ if family == .systemLarge && !entry.quoteAuthor.isEmpty {
159
+ // Attribution on its own line, right-aligned and bold so
160
+ // it reads as the source of the quote rather than part of
161
+ // the quote itself.
162
+ Text(entry.quoteAuthor)
163
+ .font(.system(size: 13, weight: .bold, design: .rounded))
164
+ .foregroundStyle(.white.opacity(0.7))
165
+ .frame(maxWidth: .infinity, alignment: .trailing)
166
+ .padding(.trailing, 8)
167
+ .padding(.top, 4)
168
+ }
123
169
  }
124
170
 
125
171
  Spacer()
@@ -133,10 +179,10 @@ struct MyWidgetWidgetView: View {
133
179
  if entry.isPro {
134
180
  Label(entry.planText, systemImage: "star.fill")
135
181
  .font(.system(size: 11, weight: .bold, design: .rounded))
136
- .foregroundStyle(Color(red: 1.0, green: 0.84, blue: 0.0))
182
+ .foregroundStyle(WidgetBrand.proGold)
137
183
  .padding(.horizontal, 10)
138
184
  .padding(.vertical, 5)
139
- .background(Color(red: 1.0, green: 0.84, blue: 0.0).opacity(0.18))
185
+ .background(WidgetBrand.proPillBg)
140
186
  .clipShape(Capsule())
141
187
  } else {
142
188
  Text(entry.planText)
@@ -144,7 +190,7 @@ struct MyWidgetWidgetView: View {
144
190
  .foregroundStyle(.white.opacity(0.45))
145
191
  .padding(.horizontal, 10)
146
192
  .padding(.vertical, 5)
147
- .background(Color.white.opacity(0.08))
193
+ .background(WidgetBrand.freePillBg)
148
194
  .clipShape(Capsule())
149
195
  }
150
196
  }
@@ -153,7 +199,7 @@ struct MyWidgetWidgetView: View {
153
199
  Spacer(minLength: 12)
154
200
  ZStack {
155
201
  Circle()
156
- .fill(Color.white.opacity(0.18))
202
+ .fill(WidgetBrand.addButtonBg)
157
203
  .frame(width: 34, height: 34)
158
204
  Image(systemName: "plus")
159
205
  .font(.system(size: 16, weight: .bold))
@@ -176,8 +222,8 @@ struct MyWidgetWidget: Widget {
176
222
  .containerBackground(for: .widget) {
177
223
  LinearGradient(
178
224
  gradient: Gradient(colors: [
179
- Color(red: 0.08, green: 0.03, blue: 0.16),
180
- Color(red: 0.20, green: 0.09, blue: 0.42),
225
+ WidgetBrand.gradientStart,
226
+ WidgetBrand.gradientEnd,
181
227
  ]),
182
228
  startPoint: .topLeading,
183
229
  endPoint: .bottomTrailing
@@ -193,17 +239,17 @@ struct MyWidgetWidget: Widget {
193
239
  #Preview("Small", as: .systemSmall) {
194
240
  MyWidgetWidget()
195
241
  } timeline: {
196
- MyWidgetEntry(date: .now, greeting: "Bom dia", title: "Olá, Paulo!", planText: "PRO", isPro: true, quote: "")
242
+ MyWidgetEntry(date: .now, greeting: "Bom dia", title: "Olá, Paulo!", planText: "PRO", isPro: true, quote: "Seu tempo é limitado.", quoteAuthor: "")
197
243
  }
198
244
 
199
245
  #Preview("Medium", as: .systemMedium) {
200
246
  MyWidgetWidget()
201
247
  } timeline: {
202
- MyWidgetEntry(date: .now, greeting: "Boa tarde", title: "Olá, Paulo!", planText: "Plano grátis", isPro: false, quote: "")
248
+ MyWidgetEntry(date: .now, greeting: "Boa tarde", title: "Olá, Paulo!", planText: "Plano grátis", isPro: false, quote: "Seu tempo é limitado.\nNão viva a vida de outra pessoa.", quoteAuthor: "")
203
249
  }
204
250
 
205
251
  #Preview("Large", as: .systemLarge) {
206
252
  MyWidgetWidget()
207
253
  } timeline: {
208
- MyWidgetEntry(date: .now, greeting: "Boa noite", title: "Olá, Paulo!", planText: "PRO", isPro: true, quote: "Sempre parece impossível, até que seja feito.")
254
+ MyWidgetEntry(date: .now, greeting: "Boa noite", title: "Olá, Paulo!", planText: "PRO", isPro: true, quote: "Seu tempo é limitado.\nNão viva a vida de outra pessoa.\nTenha coragem de seguir sua intuição.\nTodo o resto é secundário.", quoteAuthor: "Steve Jobs")
209
255
  }
@@ -38,7 +38,7 @@
38
38
  </scene>
39
39
  </scenes>
40
40
  <resources>
41
- <image name="LaunchImage" width="512" height="512"/>
41
+ <image name="LaunchImage" width="1254" height="1254"/>
42
42
  <image name="LaunchBackground" width="1" height="1"/>
43
43
  </resources>
44
44
  </document>
@@ -18,6 +18,7 @@ export 'kasy_button.dart';
18
18
  export 'kasy_card.dart';
19
19
  export 'kasy_checkbox.dart';
20
20
  export 'kasy_chip.dart';
21
+ export 'kasy_date_picker.dart';
21
22
  export 'kasy_dialog.dart';
22
23
  export 'kasy_otp_verification_bottom_sheet.dart';
23
24
  export 'kasy_skeleton.dart';
@@ -1,7 +1,8 @@
1
- import 'dart:ui' show lerpDouble;
1
+ import 'dart:ui' as ui show lerpDouble;
2
2
 
3
3
  import 'package:flutter/foundation.dart' show kIsWeb;
4
4
  import 'package:flutter/material.dart';
5
+ import 'package:kasy_kit/components/kasy_avatar_presets.dart';
5
6
  import 'package:kasy_kit/core/theme/theme.dart';
6
7
 
7
8
  /// Logical sizes for [KasyAvatar] (diameter in logical pixels).
@@ -45,7 +46,7 @@ class KasyAvatar extends StatelessWidget {
45
46
  final bool showStoryRing;
46
47
  final Gradient? storyRingGradient;
47
48
  final KasyAvatarStatus? status;
48
- final Gradient? backgroundGradient;
49
+ final KasyAvatarGradientData? backgroundGradient;
49
50
  final VoidCallback? onTap;
50
51
  final String? semanticLabel;
51
52
 
@@ -73,7 +74,7 @@ class KasyAvatar extends StatelessWidget {
73
74
  factory KasyAvatar.gradientFill({
74
75
  Key? key,
75
76
  required KasyAvatarSize size,
76
- required Gradient gradient,
77
+ required KasyAvatarGradientData gradient,
77
78
  double? diameter,
78
79
  bool showShadow = true,
79
80
  VoidCallback? onTap,
@@ -183,17 +184,7 @@ class KasyAvatar extends StatelessWidget {
183
184
  return Stack(
184
185
  fit: StackFit.expand,
185
186
  children: [
186
- DecoratedBox(
187
- decoration: BoxDecoration(
188
- shape: shape == KasyAvatarShape.circle
189
- ? BoxShape.circle
190
- : BoxShape.rectangle,
191
- borderRadius: shape == KasyAvatarShape.roundedSquare
192
- ? BorderRadius.circular(_d * 0.32)
193
- : null,
194
- gradient: backgroundGradient,
195
- ),
196
- ),
187
+ CustomPaint(painter: _AvatarOrbPainter(backgroundGradient!)),
197
188
  if (initialsRaw != null && initialsRaw.isNotEmpty)
198
189
  Center(
199
190
  child: Text(
@@ -497,6 +488,63 @@ _KasyAvatarColors _colorsForTone(
497
488
  return _KasyAvatarColors(background: bg, foreground: a);
498
489
  }
499
490
 
491
+ // ─────────────────────────────────────────────────────────────────────────────
492
+ // Orb painter
493
+ // ─────────────────────────────────────────────────────────────────────────────
494
+
495
+ /// Renders the HeroUI-style orb effect: a soft linear-gradient background
496
+ /// plus a blurred sphere in the lower-center, using geometry extracted from
497
+ /// the Figma SVGs (all 14 presets share identical shape, only colors differ).
498
+ class _AvatarOrbPainter extends CustomPainter {
499
+ final KasyAvatarGradientData data;
500
+
501
+ const _AvatarOrbPainter(this.data);
502
+
503
+ @override
504
+ void paint(Canvas canvas, Size size) {
505
+ // Background: linear gradient top → bottom.
506
+ final Rect rect = Offset.zero & size;
507
+ final Paint bgPaint = Paint()
508
+ ..shader = LinearGradient(
509
+ begin: Alignment.topCenter,
510
+ end: Alignment.bottomCenter,
511
+ colors: [data.bgTop, data.bgBottom],
512
+ ).createShader(rect);
513
+ canvas.drawRect(rect, bgPaint);
514
+
515
+ // Orb: blurred circle with diagonal linear gradient.
516
+ // Geometry normalised from 80x80 px Figma SVG:
517
+ // center = (45.625, 52.5) → (0.5703 w, 0.6563 h)
518
+ // radius = 27.5 → 0.344 w
519
+ // blur = stdDeviation 7.8125 → 0.0977 w
520
+ final Offset center = Offset(
521
+ size.width * 0.5703,
522
+ size.height * 0.6563,
523
+ );
524
+ final double radius = size.width * 0.344;
525
+ final double blurSigma = size.width * 0.0977;
526
+
527
+ // Gradient aligned to the orb bounding box.
528
+ final Rect orbRect = Rect.fromCircle(center: center, radius: radius);
529
+ final Paint orbPaint = Paint()
530
+ ..shader = LinearGradient(
531
+ begin: const Alignment(-0.24, 0.0),
532
+ end: const Alignment(0.75, 1.21),
533
+ colors: [data.orbTop, data.orbBottom],
534
+ ).createShader(orbRect)
535
+ ..maskFilter = MaskFilter.blur(BlurStyle.normal, blurSigma);
536
+
537
+ canvas.drawCircle(center, radius, orbPaint);
538
+ }
539
+
540
+ @override
541
+ bool shouldRepaint(_AvatarOrbPainter old) => data != old.data;
542
+ }
543
+
544
+ // ─────────────────────────────────────────────────────────────────────────────
545
+ // Pressable wrapper
546
+ // ─────────────────────────────────────────────────────────────────────────────
547
+
500
548
  class _KasyAvatarPressable extends StatefulWidget {
501
549
  final Widget child;
502
550
  final VoidCallback onPressed;
@@ -538,12 +586,12 @@ class _KasyAvatarPressableState extends State<_KasyAvatarPressable>
538
586
  double get _s {
539
587
  final double t = _c.value.clamp(0.0, 1.0);
540
588
  if (t <= 0.28) {
541
- return lerpDouble(1.0, _pressIn, t / 0.28)!;
589
+ return ui.lerpDouble(1.0, _pressIn, t / 0.28)!;
542
590
  }
543
591
  if (t <= 0.55) {
544
- return lerpDouble(_pressIn, _releasePeak, (t - 0.28) / 0.27)!;
592
+ return ui.lerpDouble(_pressIn, _releasePeak, (t - 0.28) / 0.27)!;
545
593
  }
546
- return lerpDouble(_releasePeak, 1.0, (t - 0.55) / 0.45)!;
594
+ return ui.lerpDouble(_releasePeak, 1.0, (t - 0.55) / 0.45)!;
547
595
  }
548
596
 
549
597
  void _tap() {