kasy-cli 1.17.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 (90) hide show
  1. package/bin/kasy.js +15 -2
  2. package/lib/commands/add.js +7 -7
  3. package/lib/commands/configure.js +548 -0
  4. package/lib/commands/deploy.js +4 -4
  5. package/lib/commands/doctor.js +17 -0
  6. package/lib/commands/favicon.js +4 -4
  7. package/lib/commands/icon.js +5 -5
  8. package/lib/commands/new.js +403 -238
  9. package/lib/commands/run.js +1 -1
  10. package/lib/commands/splash.js +5 -5
  11. package/lib/commands/update.js +9 -9
  12. package/lib/scaffold/CHANGELOG.json +14 -0
  13. package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +108 -0
  14. package/lib/scaffold/backends/firebase/setup-from-scratch.js +44 -5
  15. package/lib/scaffold/generate.js +24 -8
  16. package/lib/scaffold/shared/post-build.js +8 -0
  17. package/lib/utils/brand.js +16 -12
  18. package/lib/utils/flutter-run.js +139 -11
  19. package/lib/utils/i18n/messages-en.js +58 -5
  20. package/lib/utils/i18n/messages-es.js +58 -5
  21. package/lib/utils/i18n/messages-pt.js +59 -6
  22. package/lib/utils/ui.js +79 -4
  23. package/package.json +1 -1
  24. package/templates/firebase/README.en.md +1 -1
  25. package/templates/firebase/README.es.md +1 -1
  26. package/templates/firebase/README.md +1 -1
  27. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +0 -15
  28. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +65 -26
  29. package/templates/firebase/android/app/src/main/res/drawable/widget_add_button.xml +2 -2
  30. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +7 -2
  31. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +6 -6
  32. package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +1 -1
  33. package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +2 -2
  34. package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +1 -1
  35. package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
  36. package/templates/firebase/android/app/src/main/res/drawable-hdpi/splash.png +0 -0
  37. package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
  38. package/templates/firebase/android/app/src/main/res/drawable-mdpi/splash.png +0 -0
  39. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
  40. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/splash.png +0 -0
  41. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
  42. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/splash.png +0 -0
  43. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
  44. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/splash.png +0 -0
  45. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
  46. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/splash.png +0 -0
  47. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
  48. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/splash.png +0 -0
  49. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
  50. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/splash.png +0 -0
  51. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
  52. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/splash.png +0 -0
  53. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
  54. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/splash.png +0 -0
  55. package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +3 -3
  56. package/templates/firebase/android/app/src/main/res/values/colors.xml +32 -0
  57. package/templates/firebase/assets/images/splash_logo_dark.png +0 -0
  58. package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
  59. package/templates/firebase/assets/images/splash_logo_light.png +0 -0
  60. package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
  61. package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +75 -29
  62. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
  63. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
  64. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
  65. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png +0 -0
  66. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png +0 -0
  67. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png +0 -0
  68. package/templates/firebase/ios/Runner/Base.lproj/LaunchScreen.storyboard +1 -1
  69. package/templates/firebase/lib/components/components.dart +1 -0
  70. package/templates/firebase/lib/components/kasy_avatar.dart +65 -17
  71. package/templates/firebase/lib/components/kasy_avatar_presets.dart +121 -97
  72. package/templates/firebase/lib/components/kasy_button.dart +8 -8
  73. package/templates/firebase/lib/components/kasy_date_picker.dart +834 -0
  74. package/templates/firebase/lib/components/kasy_tabs.dart +145 -61
  75. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +12 -18
  76. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +565 -77
  77. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +10 -5
  78. package/templates/firebase/lib/i18n/en.i18n.json +2 -1
  79. package/templates/firebase/lib/i18n/es.i18n.json +2 -1
  80. package/templates/firebase/lib/i18n/pt.i18n.json +2 -1
  81. package/templates/firebase/pubspec.yaml +1 -1
  82. package/templates/firebase/web/index.html +9 -0
  83. package/templates/firebase/web/splash/img/dark-1x.png +0 -0
  84. package/templates/firebase/web/splash/img/dark-2x.png +0 -0
  85. package/templates/firebase/web/splash/img/dark-3x.png +0 -0
  86. package/templates/firebase/web/splash/img/dark-4x.png +0 -0
  87. package/templates/firebase/web/splash/img/light-1x.png +0 -0
  88. package/templates/firebase/web/splash/img/light-2x.png +0 -0
  89. package/templates/firebase/web/splash/img/light-3x.png +0 -0
  90. package/templates/firebase/web/splash/img/light-4x.png +0 -0
package/lib/utils/ui.js CHANGED
@@ -91,8 +91,20 @@ function cancel(message) { clack.cancel(message); }
91
91
  * Spinner integrated with Clack's vertical line (│).
92
92
  * Returns: { start(msg), message(msg), stop(msg, code?) }
93
93
  * code: 0 = success (✦), 1 = cancel (■), 2 = error (▲)
94
+ *
95
+ * Optional `color` paints every text passed through (start/message/stop) —
96
+ * pass `paintLime` (from utils/brand) for the Kasy brand color.
94
97
  */
95
- function spinner() { return clack.spinner(); }
98
+ function spinner({ color } = {}) {
99
+ const s = clack.spinner();
100
+ if (typeof color !== 'function') return s;
101
+ return {
102
+ start(msg) { s.start(msg != null ? color(msg) : msg); },
103
+ message(msg) { s.message(msg != null ? color(msg) : msg); },
104
+ stop(msg, code) { s.stop(msg != null ? color(msg) : msg, code); },
105
+ error(msg) { s.error(msg != null ? color(msg) : msg); },
106
+ };
107
+ }
96
108
 
97
109
  /**
98
110
  * Spinner with an automatic elapsed-time suffix that ticks every second.
@@ -104,16 +116,18 @@ function spinner() { return clack.spinner(); }
104
116
  * // ... 73 seconds later
105
117
  * s.stop('Deploy done'); // "Deploy done [1m 13s]"
106
118
  */
107
- function timedSpinner() {
119
+ function timedSpinner({ color } = {}) {
108
120
  const s = clack.spinner();
121
+ const paint = typeof color === 'function' ? color : (t) => t;
109
122
  let startTime = null;
110
123
  let currentMessage = '';
111
124
  let tick = null;
112
125
 
113
126
  const render = (msg) => {
114
127
  if (!msg) return '';
115
- if (!startTime) return msg;
116
- return `${msg} ${kleur.dim(`[${formatElapsedSeconds(startTime)}]`)}`;
128
+ const painted = paint(msg);
129
+ if (!startTime) return painted;
130
+ return `${painted} ${kleur.dim(`[${formatElapsedSeconds(startTime)}]`)}`;
117
131
  };
118
132
 
119
133
  const stopTick = () => {
@@ -197,6 +211,66 @@ function makeTimedStepper() {
197
211
  };
198
212
  }
199
213
 
214
+ /**
215
+ * Single-line stepper for Quick mode. Same API as makeTimedStepper, but every
216
+ * `.next(text)` mutates the message of a single underlying spinner instead of
217
+ * closing it and opening a new one — so the user sees one line that keeps
218
+ * updating ("Creating Firebase project…" → "Waiting for propagation…" → …)
219
+ * with a running total timer instead of a tall stack of step lines.
220
+ *
221
+ * Optional `color` paints the running text (lime in Kasy Quick mode). The
222
+ * final success/fail message keeps the caller-supplied formatting untouched.
223
+ */
224
+ function makeQuickStepper({ color } = {}) {
225
+ const paint = typeof color === 'function' ? color : (t) => t;
226
+ const s = timedSpinner();
227
+ let started = false;
228
+ let currentMsg = '';
229
+ return {
230
+ next(text) {
231
+ currentMsg = text;
232
+ if (!started) {
233
+ s.start(paint(text));
234
+ started = true;
235
+ } else {
236
+ s.message(paint(text));
237
+ }
238
+ },
239
+ update(text) {
240
+ currentMsg = text;
241
+ if (started) s.message(paint(text));
242
+ },
243
+ succeed(text) {
244
+ if (started) {
245
+ s.stop(paint(text || currentMsg));
246
+ started = false;
247
+ currentMsg = '';
248
+ }
249
+ },
250
+ fail(text) {
251
+ if (started) {
252
+ s.error(text || currentMsg);
253
+ started = false;
254
+ currentMsg = '';
255
+ }
256
+ },
257
+ warn(text) {
258
+ if (started) {
259
+ s.stop(`⚠ ${text || currentMsg}`);
260
+ started = false;
261
+ currentMsg = '';
262
+ }
263
+ },
264
+ stop() {
265
+ if (started) {
266
+ s.stop(paint(currentMsg));
267
+ started = false;
268
+ currentMsg = '';
269
+ }
270
+ },
271
+ };
272
+ }
273
+
200
274
  /**
201
275
  * Multi-step spinner: each .next(text) succeeds the previous step
202
276
  * with the previous message, then starts a new step with `text`.
@@ -281,6 +355,7 @@ module.exports = {
281
355
  timedSpinner,
282
356
  makeStepper,
283
357
  makeTimedStepper,
358
+ makeQuickStepper,
284
359
  taskLog,
285
360
  progress,
286
361
  log,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "CLI for scaffolding production-ready Flutter SaaS apps with Firebase, Supabase, or API REST backends.",
5
5
  "bin": {
6
6
  "kasy": "./bin/kasy.js"
@@ -185,6 +185,6 @@ No Mac: [docs/codemagic-release.md](docs/codemagic-release.md)
185
185
 
186
186
  ## Security
187
187
 
188
- `.gitignore` already excludes: `firebase_key.json`, `.env`, `.env.*`, `*.pem`, `*.keystore`, `.kasy/apple.env`, `.kasy/codemagic.env`.
188
+ `.gitignore` already excludes: `firebase_key.json`, `.env`, `.env.*`, `*.pem`, `*.keystore`, `.kasy/apple.env`, `.kasy/codemagic.env`, `.kasy/*.log`.
189
189
 
190
190
  Never commit credentials to the repository.
@@ -185,6 +185,6 @@ Sin Mac: [docs/codemagic-release.md](docs/codemagic-release.md)
185
185
 
186
186
  ## Seguridad
187
187
 
188
- El `.gitignore` ya excluye: `firebase_key.json`, `.env`, `.env.*`, `*.pem`, `*.keystore`, `.kasy/apple.env`, `.kasy/codemagic.env`.
188
+ El `.gitignore` ya excluye: `firebase_key.json`, `.env`, `.env.*`, `*.pem`, `*.keystore`, `.kasy/apple.env`, `.kasy/codemagic.env`, `.kasy/*.log`.
189
189
 
190
190
  Nunca subas credenciales al repositorio.
@@ -185,6 +185,6 @@ Sem Mac: [docs/codemagic-release.md](docs/codemagic-release.md)
185
185
 
186
186
  ## Segurança
187
187
 
188
- O `.gitignore` já exclui: `firebase_key.json`, `.env`, `.env.*`, `*.pem`, `*.keystore`, `.kasy/apple.env`, `.kasy/codemagic.env`.
188
+ O `.gitignore` já exclui: `firebase_key.json`, `.env`, `.env.*`, `*.pem`, `*.keystore`, `.kasy/apple.env`, `.kasy/codemagic.env`, `.kasy/*.log`.
189
189
 
190
190
  Nunca comite credenciais no repositório.
@@ -1,9 +1,7 @@
1
1
  package com.aicrus.firebase.kit
2
2
 
3
3
  import android.content.Context
4
- import android.content.Intent
5
4
  import android.os.Bundle
6
- import android.util.Log
7
5
  import androidx.appcompat.app.AppCompatDelegate
8
6
  import com.google.android.gms.ads.identifier.AdvertisingIdClient
9
7
  import io.flutter.embedding.android.FlutterActivity
@@ -22,19 +20,6 @@ class MainActivity : FlutterActivity() {
22
20
  super.onCreate(savedInstanceState)
23
21
  }
24
22
 
25
- // Logs the incoming intent so we can diagnose warm-starts from the home
26
- // widget that land on go_router's 404. With this, the Dart side's onException
27
- // logs and these adb logs together show whether the intent itself carries a
28
- // bad URI (data Uri / extras) or whether the 404 comes from elsewhere.
29
- override fun onNewIntent(intent: Intent) {
30
- Log.d(
31
- "KasyWidgetTap",
32
- "onNewIntent action=${intent.action} data=${intent.data} " +
33
- "flags=0x${Integer.toHexString(intent.flags)} extras=${intent.extras}",
34
- )
35
- super.onNewIntent(intent)
36
- }
37
-
38
23
  // Forces the night mode to match the user's saved theme preference (read
39
24
  // from `shared_preferences`) so the native splash drawable selection
40
25
  // (drawable-night vs drawable) follows the in-app choice, not just the OS.
@@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
5
5
  import androidx.compose.ui.graphics.Color
6
6
  import androidx.compose.ui.unit.dp
7
7
  import androidx.compose.ui.unit.sp
8
+ import androidx.core.content.ContextCompat
8
9
  import androidx.glance.GlanceId
9
10
  import androidx.glance.GlanceModifier
10
11
  import androidx.glance.Image
@@ -35,7 +36,6 @@ import androidx.glance.text.TextStyle
35
36
  import androidx.glance.unit.ColorProvider
36
37
  import es.antonborri.home_widget.HomeWidgetGlanceState
37
38
  import es.antonborri.home_widget.HomeWidgetGlanceStateDefinition
38
- import java.util.Calendar
39
39
  import java.util.Locale
40
40
 
41
41
  class MyWidgetWidget : GlanceAppWidget() {
@@ -61,24 +61,50 @@ class MyWidgetWidget : GlanceAppWidget() {
61
61
  val planText = prefs.getString("planText", "") ?: ""
62
62
  val isPro = prefs.getString("isPro", "false") == "true"
63
63
  val quote = prefs.getString("quote", "") ?: ""
64
+ val quoteAuthor = prefs.getString("quoteAuthor", "") ?: ""
64
65
 
65
- // Time/locale-based fallback used when Flutter has not pushed data yet —
66
- // first install before the app opens. Keeps the widget from rendering
67
- // blank in the gallery preview.
66
+ // Fallback used when Flutter has not pushed data yet — first install
67
+ // before the app opens. Keeps the widget from rendering blank in the
68
+ // gallery preview.
68
69
  val defaults = defaultStrings()
69
70
  val greeting = storedGreeting.ifEmpty { defaults.greeting }
70
71
  val title = storedTitle.ifEmpty { defaults.hello }
71
72
 
72
73
  val size = LocalSize.current
73
- val isSmall = size.width < 200.dp
74
- // Heuristic: tall enough to fit greeting + title + quote without crowding.
75
- val isLarge = size.height >= 280.dp
74
+ // "Narrow" covers both the true small widget and the tall-but-narrow
75
+ // single-column shape both get the tightest sentence count.
76
+ val isSmall = size.width < 240.dp
77
+ // We show N COMPLETE sentences from the quote (split on "\n" — which is
78
+ // how the i18n JSON separates the four sentences). Truncating per
79
+ // sentence avoids the ugly "…" mid-word that lineLimit alone produces.
80
+ // height < 180dp → no quote.
81
+ // narrow column → 1 sentence (just the first one).
82
+ // wide + short → 2 sentences.
83
+ // wide + medium height → 3 sentences.
84
+ // wide + tall → 4 sentences + bold author.
85
+ val showQuote = size.height >= 180.dp
86
+ val numSentences = when {
87
+ isSmall -> 1
88
+ size.height < 240.dp -> 2
89
+ size.height < 300.dp -> 3
90
+ else -> 4
91
+ }
92
+ val showQuoteAuthor = !isSmall && size.height >= 300.dp
93
+ val sentences = quote.split("\n").filter { it.isNotBlank() }
94
+ val displayQuote = sentences.take(numSentences).joinToString("\n")
95
+ // Each sentence is short enough to wrap to 2 lines on narrow widgets,
96
+ // so a 2x cap covers the worst case without slicing the last sentence.
97
+ val quoteMaxLines = numSentences * 2
76
98
 
99
+ // Brand colors. The pure-Compose colors (whiteSubtle, whiteQuote,
100
+ // whiteVerySubtle) are foreground tints used by the live widget only,
101
+ // so they stay inline. The gold pulls from res/values/colors.xml to keep
102
+ // a single source of truth shared with the XML drawables.
77
103
  val white = Color.White
78
104
  val whiteSubtle = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.55f)
79
105
  val whiteQuote = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.65f)
80
- val gold = Color(red = 1f, green = 0.84f, blue = 0f)
81
106
  val whiteVerySubtle = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.45f)
107
+ val gold = Color(ContextCompat.getColor(context, R.color.widget_pro_gold))
82
108
 
83
109
  // The gradient lives in its own Image at the bottom of the stack rather
84
110
  // than as `.background(ImageProvider(...))`, because in some Glance
@@ -116,18 +142,37 @@ class MyWidgetWidget : GlanceAppWidget() {
116
142
  ),
117
143
  modifier = GlanceModifier.padding(top = 4.dp),
118
144
  )
119
- if (isLarge && quote.isNotEmpty()) {
145
+ if (showQuote && displayQuote.isNotEmpty()) {
120
146
  Text(
121
- text = quote,
147
+ text = displayQuote,
122
148
  style = TextStyle(
123
149
  color = ColorProvider(whiteQuote),
124
150
  fontSize = 15.sp,
125
151
  fontWeight = FontWeight.Normal,
126
152
  fontStyle = FontStyle.Italic,
127
153
  ),
128
- maxLines = 4,
154
+ maxLines = quoteMaxLines,
129
155
  modifier = GlanceModifier.padding(top = 12.dp),
130
156
  )
157
+ if (showQuoteAuthor && quoteAuthor.isNotEmpty()) {
158
+ // Author goes on its own line, right-aligned and bold, so the
159
+ // quote reads as a quote and the attribution is visually distinct.
160
+ Row(
161
+ modifier = GlanceModifier
162
+ .fillMaxWidth()
163
+ .padding(top = 4.dp),
164
+ horizontalAlignment = Alignment.End,
165
+ ) {
166
+ Text(
167
+ text = quoteAuthor,
168
+ style = TextStyle(
169
+ color = ColorProvider(whiteQuote),
170
+ fontSize = 13.sp,
171
+ fontWeight = FontWeight.Bold,
172
+ ),
173
+ )
174
+ }
175
+ }
131
176
  }
132
177
  Spacer(modifier = GlanceModifier.defaultWeight())
133
178
  Row(
@@ -200,22 +245,16 @@ class MyWidgetWidget : GlanceAppWidget() {
200
245
  }
201
246
  }
202
247
 
248
+ // Fallback used ONLY in the brief window between the widget being placed and
249
+ // the Flutter app pushing real values. Keep this dead simple — the
250
+ // time-aware greeting in three languages lives on the Dart side
251
+ // (home_widget_mywidget_service.dart::_greeting). Duplicating that logic
252
+ // here was a maintenance trap when adding new locales.
203
253
  private data class DefaultStrings(val greeting: String, val hello: String)
204
254
 
205
- private fun defaultStrings(): DefaultStrings {
206
- val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
207
- val lang = Locale.getDefault().language
208
- val morning: String; val afternoon: String; val evening: String; val hello: String
209
- when (lang) {
210
- "pt" -> { morning = "Bom dia"; afternoon = "Boa tarde"; evening = "Boa noite"; hello = "Olá!" }
211
- "es" -> { morning = "Buenos días"; afternoon = "Buenas tardes"; evening = "Buenas noches"; hello = "¡Hola!" }
212
- else -> { morning = "Good morning"; afternoon = "Good afternoon"; evening = "Good evening"; hello = "Hi there!" }
213
- }
214
- val greeting = when {
215
- hour < 12 -> morning
216
- hour < 18 -> afternoon
217
- else -> evening
218
- }
219
- return DefaultStrings(greeting, hello)
255
+ private fun defaultStrings(): DefaultStrings = when (Locale.getDefault().language) {
256
+ "pt" -> DefaultStrings(greeting = "Olá", hello = "Bem-vindo!")
257
+ "es" -> DefaultStrings(greeting = "Hola", hello = "¡Bienvenido!")
258
+ else -> DefaultStrings(greeting = "Hello", hello = "Welcome!")
220
259
  }
221
260
  }
@@ -6,10 +6,10 @@
6
6
  android:viewportHeight="34">
7
7
  <path
8
8
  android:pathData="M17,0 A17,17 0 1,0 17,34 A17,17 0 1,0 17,0 Z"
9
- android:fillColor="#2EFFFFFF"/>
9
+ android:fillColor="@color/widget_add_button_bg"/>
10
10
  <path
11
11
  android:pathData="M17,9 L17,25 M9,17 L25,17"
12
- android:strokeColor="#FFFFFFFF"
12
+ android:strokeColor="@color/widget_add_button_stroke"
13
13
  android:strokeWidth="2.5"
14
14
  android:strokeLineCap="round"/>
15
15
  </vector>
@@ -1,9 +1,14 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Background used by the static preview / loading layouts (RemoteViews
3
+ XML rendered by the launcher BEFORE Glance composes the real widget).
4
+ Includes the corner radius because the launcher doesn't clip these
5
+ layouts the same way it clips the live Glance widget.
6
+ To restyle the brand gradient, edit res/values/colors.xml. -->
2
7
  <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
3
8
  <gradient
4
9
  android:angle="315"
5
- android:startColor="#FF140829"
6
- android:endColor="#FF33176B"
10
+ android:startColor="@color/widget_gradient_start"
11
+ android:endColor="@color/widget_gradient_end"
7
12
  android:type="linear" />
8
13
  <corners android:radius="24dp" />
9
14
  </shape>
@@ -1,12 +1,12 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
- <!-- Same gradient as widget_gradient_bg, but without rounded corners.
3
- Used as the background of the Glance widget itself the system
4
- already clips the widget with the OS-provided corner radius, so
5
- adding corners here would cause a visible double-radius edge. -->
2
+ <!-- Background of the live Glance widget. No corners here — the OS already
3
+ clips the widget with its corner radius, so adding rounded corners
4
+ would produce a visible double-radius edge. To restyle the brand
5
+ gradient, edit res/values/colors.xml. -->
6
6
  <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
7
7
  <gradient
8
8
  android:angle="315"
9
- android:startColor="#FF140829"
10
- android:endColor="#FF33176B"
9
+ android:startColor="@color/widget_gradient_start"
10
+ android:endColor="@color/widget_gradient_end"
11
11
  android:type="linear" />
12
12
  </shape>
@@ -1,5 +1,5 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
3
3
  <corners android:radius="999dp"/>
4
- <solid android:color="#14FFFFFF"/>
4
+ <solid android:color="@color/widget_free_pill_bg"/>
5
5
  </shape>
@@ -8,8 +8,8 @@
8
8
  <shape android:shape="rectangle">
9
9
  <gradient
10
10
  android:angle="315"
11
- android:startColor="#FF140829"
12
- android:endColor="#FF33176B"
11
+ android:startColor="@color/widget_gradient_start"
12
+ android:endColor="@color/widget_gradient_end"
13
13
  android:type="linear" />
14
14
  <corners android:radius="24dp" />
15
15
  </shape>
@@ -1,5 +1,5 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
3
3
  <corners android:radius="999dp"/>
4
- <solid android:color="#2EFFD700"/>
4
+ <solid android:color="@color/widget_pro_pill_bg"/>
5
5
  </shape>
@@ -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>