kasy-cli 1.19.3 → 1.20.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 (43) hide show
  1. package/bin/kasy.js +1 -0
  2. package/lib/commands/new.js +9 -0
  3. package/lib/commands/run.js +7 -0
  4. package/lib/scaffold/backends/firebase/setup-from-scratch.js +91 -2
  5. package/lib/scaffold/engine.js +5 -0
  6. package/lib/scaffold/generate.js +4 -0
  7. package/lib/scaffold/shared/generator-utils.js +38 -1
  8. package/lib/utils/i18n/messages-en.js +1 -0
  9. package/lib/utils/i18n/messages-es.js +1 -0
  10. package/lib/utils/i18n/messages-pt.js +1 -0
  11. package/package.json +1 -1
  12. package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
  13. package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
  14. package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
  15. package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
  16. package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
  17. package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
  18. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
  19. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
  20. package/templates/firebase/lib/components/kasy_date_picker.dart +14 -8
  21. package/templates/firebase/lib/components/kasy_sidebar_pro.dart +1148 -0
  22. package/templates/firebase/lib/components/kasy_tabs.dart +156 -43
  23. package/templates/firebase/lib/components/kasy_text_field.dart +37 -34
  24. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +90 -69
  25. package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +30 -7
  26. package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +8 -1
  27. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +433 -243
  28. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +198 -83
  29. package/templates/firebase/lib/core/icons/kasy_icons.dart +1 -0
  30. package/templates/firebase/lib/core/theme/colors.dart +6 -2
  31. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +119 -19
  32. package/templates/firebase/lib/core/widgets/kasy_hover.dart +68 -27
  33. package/templates/firebase/lib/features/home/home_components_page.dart +3 -0
  34. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +121 -66
  35. package/templates/firebase/lib/features/settings/settings_page.dart +27 -146
  36. package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +16 -3
  37. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +22 -5
  38. package/templates/firebase/lib/i18n/en.i18n.json +3 -1
  39. package/templates/firebase/lib/i18n/es.i18n.json +3 -1
  40. package/templates/firebase/lib/i18n/pt.i18n.json +3 -1
  41. package/templates/firebase/pubspec.yaml +6 -4
  42. package/templates/firebase/web/index.html +7 -17
  43. package/templates/firebase/lib/firebase_options.dart +0 -75
package/bin/kasy.js CHANGED
@@ -357,6 +357,7 @@ function buildProgram(language) {
357
357
  .option('--web', 'Run on web — prints localhost URL in lime so you open it in your own browser (your extensions, your accounts)')
358
358
  .option('--open', 'With --web: auto-launch a clean Chrome window (Flutter profile, no extensions) instead of just printing the URL')
359
359
  .option('--web-port <port>', 'Fixed port for web (default 5555) — keeps the origin stable so Firebase Auth sessions persist between runs')
360
+ .option('--web-hostname <host>', 'Host for web (default localhost) — localhost is a Firebase-authorized domain by default, so Google sign-in works without console changes')
360
361
  .option('-d, --device <id>', 'Run on specific device ID')
361
362
  .option('--prod', 'Use production dart-defines (from launch.json)')
362
363
  .option('--no-defines', 'Skip dart-defines from launch.json')
@@ -772,6 +772,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
772
772
  } else if (key === 'auth-providers-warn') {
773
773
  ps1.stop();
774
774
  ui.log.warn(`${tr('new.firebase.interactive.authWarn')}\n${kleur.cyan(data?.url || '')}`);
775
+ } else if (key === 'auth-localhost-warn') {
776
+ ps1.stop();
777
+ ui.log.warn(`${tr('new.firebase.localhostWarn')}\n${kleur.cyan(data?.url || '')}`);
775
778
  }
776
779
  };
777
780
  let setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
@@ -1179,6 +1182,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1179
1182
  } else if (key === 'auth-google-warn') {
1180
1183
  ps4.stop();
1181
1184
  ui.log.warn(`${tr('new.firebase.existing.googleSignInManual')}\n${kleur.cyan(data?.url || '')}`);
1185
+ } else if (key === 'auth-localhost-warn') {
1186
+ ps4.stop();
1187
+ ui.log.warn(`${tr('new.firebase.localhostWarn')}\n${kleur.cyan(data?.url || '')}`);
1182
1188
  }
1183
1189
  },
1184
1190
  });
@@ -1804,6 +1810,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1804
1810
  if (appleResult.appleEnabled) {
1805
1811
  printStepResult({ name: 'apple sign-in', ok: true }, language);
1806
1812
  }
1813
+ if (appleResult.localhostAuthorized === false) {
1814
+ ui.log.warn(`${tr('new.firebase.localhostWarn')}\n${kleur.cyan(`https://console.firebase.google.com/project/${answers.firebaseProjectId}/authentication/settings`)}`);
1815
+ }
1807
1816
  }
1808
1817
 
1809
1818
  // APNs key (iOS push) is intentionally not mentioned here — it only becomes
@@ -292,6 +292,13 @@ async function runRun(directory, options = {}) {
292
292
  }
293
293
 
294
294
  if (isChromeTarget || isWebServerTarget) {
295
+ // Pin the host to `localhost` so the app's origin is always an authorized
296
+ // domain. Firebase Auth authorizes `localhost` by default but NOT
297
+ // `127.0.0.1`, and `flutter run -d chrome` would otherwise auto-launch
298
+ // Chrome at 127.0.0.1 — which breaks Google sign-in with
299
+ // [firebase_auth/unauthorized-domain]. Forcing localhost makes Google
300
+ // login work out of the box, no Firebase Console changes required.
301
+ deviceArgs.push('--web-hostname', options.webHostname || 'localhost');
295
302
  // Pin a fixed port so the Chrome origin stays the same between runs.
296
303
  // Firebase Auth persists sessions per-origin (IndexedDB) — a random port
297
304
  // each run means the user gets logged out every restart.
@@ -659,6 +659,54 @@ async function checkBillingEnabled(projectId) {
659
659
  return { ok: true, enabled: result.stdout.trim().toLowerCase() === 'true' };
660
660
  }
661
661
 
662
+ /**
663
+ * Ensure `localhost` and `127.0.0.1` are in the project's authorized domains so
664
+ * Google/social sign-in works on Flutter web during local development.
665
+ *
666
+ * Projects created programmatically (via the Firebase Management API) do NOT get
667
+ * `localhost` seeded automatically — only `<project>.firebaseapp.com` and
668
+ * `<project>.web.app`. Without `localhost`, `signInWithPopup` fails with
669
+ * [firebase_auth/unauthorized-domain] when running `flutter run -d chrome` or
670
+ * `-d web-server`. The Firebase Console adds localhost by default; the API does
671
+ * not, so we add it here.
672
+ *
673
+ * Read-modify-write so the existing default domains are preserved — a blind PATCH
674
+ * with only [localhost] would wipe firebaseapp.com / web.app. Idempotent: it's a
675
+ * no-op when both entries are already present.
676
+ *
677
+ * @returns {{ ok: boolean, added?: string[], error?: string }}
678
+ */
679
+ async function ensureLocalhostAuthorizedDomains(projectId, token) {
680
+ const base = `https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/config`;
681
+ const headers = {
682
+ Authorization: `Bearer ${token}`,
683
+ 'Content-Type': 'application/json',
684
+ 'X-Goog-User-Project': projectId,
685
+ };
686
+ // 1. Read the current authorized domains.
687
+ const getRes = await fetch(base, { headers });
688
+ if (!getRes.ok) {
689
+ const text = await getRes.text();
690
+ return { ok: false, error: `${getRes.status}: ${text}` };
691
+ }
692
+ const config = await getRes.json();
693
+ const current = Array.isArray(config.authorizedDomains) ? config.authorizedDomains : [];
694
+ const required = ['localhost', '127.0.0.1'];
695
+ const missing = required.filter((d) => !current.includes(d));
696
+ if (missing.length === 0) return { ok: true, added: [] };
697
+ // 2. Merge and write back, keeping every domain that was already there.
698
+ const patchRes = await fetch(`${base}?updateMask=authorizedDomains`, {
699
+ method: 'PATCH',
700
+ headers,
701
+ body: JSON.stringify({ authorizedDomains: [...current, ...missing] }),
702
+ });
703
+ if (!patchRes.ok) {
704
+ const text = await patchRes.text();
705
+ return { ok: false, error: `${patchRes.status}: ${text}` };
706
+ }
707
+ return { ok: true, added: missing };
708
+ }
709
+
662
710
  /**
663
711
  * Enable Firebase Auth sign-in providers: Email/Password, Anonymous and Google.
664
712
  * Uses the Identity Toolkit Admin v2 REST API with gcloud credentials.
@@ -807,7 +855,18 @@ async function enableAuthProviders(projectId, { maxRetries = 3, retryDelayMs = 1
807
855
  // Network error — skip silently.
808
856
  }
809
857
 
810
- return { ok: true, googleSignInSkipped, googleEnabled, appleEnabled };
858
+ // Step 5: Authorize localhost / 127.0.0.1 so web sign-in works in dev.
859
+ // Best effort — failure here doesn't block the providers we just enabled.
860
+ const domainsResult = await ensureLocalhostAuthorizedDomains(projectId, token);
861
+
862
+ return {
863
+ ok: true,
864
+ googleSignInSkipped,
865
+ googleEnabled,
866
+ appleEnabled,
867
+ localhostAuthorized: domainsResult.ok,
868
+ authorizedDomainsAdded: domainsResult.added || [],
869
+ };
811
870
  }
812
871
  const text = await res.text();
813
872
  lastError = `${res.status}: ${text}`;
@@ -818,7 +877,24 @@ async function enableAuthProviders(projectId, { maxRetries = 3, retryDelayMs = 1
818
877
  }
819
878
  break;
820
879
  }
821
- return { ok: false, error: lastError };
880
+ // The provider PATCH failed after retries. Still try to authorize localhost as a
881
+ // best-effort: the auth config may already exist (a transient failure here, or
882
+ // auth initialized elsewhere), and web sign-in depends on localhost being on the
883
+ // list. This keeps the function self-sufficient instead of relying on a later
884
+ // re-invocation to backfill localhost.
885
+ let fallbackDomains = { ok: false, added: [] };
886
+ try {
887
+ const freshToken = await getAccessToken();
888
+ fallbackDomains = await ensureLocalhostAuthorizedDomains(projectId, freshToken);
889
+ } catch (_) {
890
+ // Best effort — ignore.
891
+ }
892
+ return {
893
+ ok: false,
894
+ error: lastError,
895
+ localhostAuthorized: fallbackDomains.ok,
896
+ authorizedDomainsAdded: fallbackDomains.added || [],
897
+ };
822
898
  }
823
899
 
824
900
  /**
@@ -907,6 +983,13 @@ async function setupFromScratch(appName, bundleId, options = {}) {
907
983
  url: `https://console.firebase.google.com/project/${projectId}/authentication/providers`,
908
984
  });
909
985
  }
986
+ // Providers were enabled but localhost couldn't be authorized — warn so the user
987
+ // isn't surprised by [firebase_auth/unauthorized-domain] on web sign-in.
988
+ if (authResult.ok && authResult.localhostAuthorized === false) {
989
+ onProgress('auth-localhost-warn', {
990
+ url: `https://console.firebase.google.com/project/${projectId}/authentication/settings`,
991
+ });
992
+ }
910
993
 
911
994
  onProgress('firestore');
912
995
  const firestoreResult = await createFirestoreDatabase(projectId, region);
@@ -1022,6 +1105,11 @@ async function setupExistingProject(projectId, options = {}) {
1022
1105
  url: `https://console.firebase.google.com/project/${projectId}/authentication/providers`,
1023
1106
  });
1024
1107
  }
1108
+ if (authResult.ok && authResult.localhostAuthorized === false) {
1109
+ onProgress('auth-localhost-warn', {
1110
+ url: `https://console.firebase.google.com/project/${projectId}/authentication/settings`,
1111
+ });
1112
+ }
1025
1113
 
1026
1114
  onProgress('firestore');
1027
1115
  const firestoreResult = await createFirestoreDatabase(projectId);
@@ -1114,6 +1202,7 @@ module.exports = {
1114
1202
  applyStorageCors,
1115
1203
  checkBillingEnabled,
1116
1204
  enableAuthProviders,
1205
+ ensureLocalhostAuthorizedDomains,
1117
1206
  listBillingAccounts,
1118
1207
  listGcpOrganizations,
1119
1208
  checkGcloudAuth,
@@ -41,6 +41,11 @@ const ALWAYS_EXCLUDE_BASENAMES = new Set([
41
41
  'google-services.json', // Firebase Android config — client must generate their own
42
42
  'GoogleService-Info.plist', // Firebase iOS config — client must generate their own
43
43
  'firebase_options_dev.dart', // Dev Firebase options — internal only
44
+ 'firebase_options.dart', // Kit's own Firebase options — dead code in generated
45
+ // projects (main.dart imports firebase_options_dev.dart,
46
+ // which flutterfire regenerates per-user). Shipping it
47
+ // would leak the kit's project id (fir-kit-8e56b) into
48
+ // every generated app. Never copy it.
44
49
  ]);
45
50
 
46
51
  // File extensions that may contain generated Dart code — skip these
@@ -48,6 +48,7 @@ const {
48
48
  writeEnvExample,
49
49
  writeEnvFileIfMissing,
50
50
  writeFirebaserc,
51
+ cleanFirebaseJsonKitRefs,
51
52
  writeFeaturesConfig,
52
53
  writeKitSetup,
53
54
  writeMakefile,
@@ -362,6 +363,9 @@ async function generateProject(targetDir, backend, options, hooks = {}) {
362
363
  }
363
364
 
364
365
  await writeFirebaserc(targetDir, firebaseProjectId);
366
+ // Drop the stale firebase.json entry that still points at the kit project
367
+ // (the dead lib/firebase_options.dart we no longer ship).
368
+ await cleanFirebaseJsonKitRefs(targetDir);
365
369
 
366
370
  // ── 3. Post-build específico do backend ────────────────────────────────────
367
371
  if (postBuild) {
@@ -229,6 +229,39 @@ async function writeFirebaserc(projectDir, firebaseProjectId) {
229
229
  );
230
230
  }
231
231
 
232
+ /**
233
+ * Strip the kit's own Firebase project from the generated firebase.json.
234
+ *
235
+ * flutterfire configure rewrites the android/ios/web platform entries for the
236
+ * user's project, but it keys the dart entry by its --out path
237
+ * (lib/firebase_options_dev.dart) and leaves the template's stale
238
+ * `flutter.platforms.dart["lib/firebase_options.dart"]` entry untouched — which
239
+ * still points at the kit project (fir-kit-8e56b). We no longer ship that file
240
+ * (see ALWAYS_EXCLUDE_BASENAMES), so this drops the dead entry to guarantee the
241
+ * user's firebase.json never references the kit project.
242
+ *
243
+ * Best effort: no-ops when firebase.json is missing, unparseable, or already clean.
244
+ *
245
+ * @param {string} projectDir
246
+ * @returns {Promise<{ ok: boolean, changed: boolean }>}
247
+ */
248
+ async function cleanFirebaseJsonKitRefs(projectDir) {
249
+ const firebaseJsonPath = path.join(projectDir, 'firebase.json');
250
+ if (!(await fs.pathExists(firebaseJsonPath))) return { ok: true, changed: false };
251
+ let config;
252
+ try {
253
+ config = await fs.readJson(firebaseJsonPath);
254
+ } catch {
255
+ return { ok: false, changed: false };
256
+ }
257
+ const dartMap = config && config.flutter && config.flutter.platforms && config.flutter.platforms.dart;
258
+ if (!dartMap || typeof dartMap !== 'object') return { ok: true, changed: false };
259
+ if (!('lib/firebase_options.dart' in dartMap)) return { ok: true, changed: false };
260
+ delete dartMap['lib/firebase_options.dart'];
261
+ await fs.writeJson(firebaseJsonPath, config, { spaces: 2 });
262
+ return { ok: true, changed: true };
263
+ }
264
+
232
265
  /**
233
266
  * Write environnements.dart overrides for backend-specific config.
234
267
  *
@@ -812,7 +845,10 @@ async function writeMainDart(projectDir, backend, modules, packageName, options
812
845
  lines.push(` options: firebase_dev.DefaultFirebaseOptions.currentPlatform,`);
813
846
  lines.push(` ),`);
814
847
  lines.push(` ProdEnvironment() => Firebase.initializeApp(`);
815
- lines.push(` // TODO replace with your own firebase options for production environment (if needed)`);
848
+ lines.push(` // SETUP REQUIRED: For production, create a separate Firebase project and run:`);
849
+ lines.push(` // flutterfire configure --project=<your-prod-project> --out=lib/firebase_options_prod.dart`);
850
+ lines.push(` // Then import firebase_options_prod.dart and use it here instead.`);
851
+ lines.push(` // Reusing the dev project in production is only safe for early-stage development.`);
816
852
  lines.push(` options: firebase_dev.DefaultFirebaseOptions.currentPlatform,`);
817
853
  lines.push(` ),`);
818
854
  lines.push(` };`);
@@ -1687,6 +1723,7 @@ module.exports = {
1687
1723
  writeEnvExample,
1688
1724
  writeEnvFileIfMissing,
1689
1725
  writeFirebaserc,
1726
+ cleanFirebaseJsonKitRefs,
1690
1727
  writeEnvironnementsOverrides,
1691
1728
  writeFeaturesConfig,
1692
1729
  writeKitSetup,
@@ -633,6 +633,7 @@ module.exports = {
633
633
  'new.firebase.interactive.billingWaiting': 'Checking Blaze status...',
634
634
  'new.firebase.interactive.billingTimeout': 'Blaze plan not confirmed after timeout. Deploy skipped — run manually when ready.',
635
635
  'new.firebase.interactive.authWarn': 'Could not enable Email/Password and Anonymous auth automatically. Enable manually:',
636
+ 'new.firebase.localhostWarn': 'Could not authorize localhost for web sign-in. To test login in the browser, add "localhost" under Authorized domains:',
636
637
  'new.firebase.existing.apisFailed': 'Could not activate APIs:',
637
638
  'new.firebase.existing.googleSignInManual': 'Google Sign-In: enable manually in Authentication → Sign-in method → Google',
638
639
 
@@ -633,6 +633,7 @@ module.exports = {
633
633
  'new.firebase.interactive.billingWaiting': 'Verificando estado del Blaze...',
634
634
  'new.firebase.interactive.billingTimeout': 'Plan Blaze no confirmado tras el tiempo límite. Despliegue omitido — ejecuta manualmente cuando estés listo.',
635
635
  'new.firebase.interactive.authWarn': 'No se pudo activar Email/Contraseña y Anónimo automáticamente. Actívalos manualmente:',
636
+ 'new.firebase.localhostWarn': 'No se pudo autorizar localhost para el inicio de sesión web. Para probar el login en el navegador, agrega "localhost" en Authorized domains:',
636
637
  'new.firebase.existing.apisFailed': 'No se pudieron activar las APIs:',
637
638
  'new.firebase.existing.googleSignInManual': 'Google Sign-In: activa manualmente en Authentication → Sign-in method → Google',
638
639
 
@@ -633,6 +633,7 @@ module.exports = {
633
633
  'new.firebase.interactive.billingWaiting': 'Verificando status do Blaze...',
634
634
  'new.firebase.interactive.billingTimeout': 'Plano Blaze não confirmado apos o tempo limite. Deploy ignorado — rode manualmente quando estiver pronto.',
635
635
  'new.firebase.interactive.authWarn': 'Não foi possível ativar Email/Senha e Anônimo automaticamente. Ative manualmente:',
636
+ 'new.firebase.localhostWarn': 'Não foi possível autorizar localhost para login web. Se for testar login no navegador, adicione "localhost" em Authorized domains:',
636
637
  'new.firebase.existing.apisFailed': 'Não foi possível ativar APIs:',
637
638
  'new.firebase.existing.googleSignInManual': 'Google Sign-In: ative manualmente em Authentication → Sign-in method → Google',
638
639
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.19.3",
3
+ "version": "1.20.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"
@@ -6,7 +6,7 @@
6
6
  <item name="android:windowFullscreen">true</item>
7
7
  <item name="android:windowDrawsSystemBarBackgrounds">true</item>
8
8
  <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
9
- <item name="android:windowSplashScreenBackground">#000000</item>
9
+ <item name="android:windowSplashScreenBackground">#060608</item>
10
10
  <item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
11
11
  </style>
12
12
  <!-- Theme applied to the Android Window as soon as the process has started.
@@ -6,7 +6,7 @@
6
6
  <item name="android:windowFullscreen">true</item>
7
7
  <item name="android:windowDrawsSystemBarBackgrounds">true</item>
8
8
  <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
9
- <item name="android:windowSplashScreenBackground">#FFFFFF</item>
9
+ <item name="android:windowSplashScreenBackground">#F7F7F7</item>
10
10
  <item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
11
11
  </style>
12
12
  <!-- Theme applied to the Android Window as soon as the process has started.
@@ -987,6 +987,14 @@ class _KasyDatePickerState extends State<KasyDatePicker>
987
987
  hasErrorText ? widget.errorText : widget.description;
988
988
  final Color labelColor = hasInvalidState ? c.error : c.fieldLabel;
989
989
 
990
+ // Kit-wide rule: disabled colors are softened by blending toward the
991
+ // surface (opaque), never by raw opacity. See KasyButton / KasyTextField.
992
+ final Color blendSurface = c.surface;
993
+ Color dimDisabled(Color base, {double alpha = 0.55}) {
994
+ if (!isDisabled) return base;
995
+ return Color.alphaBlend(base.withValues(alpha: alpha), blendSurface);
996
+ }
997
+
990
998
  return Column(
991
999
  crossAxisAlignment: CrossAxisAlignment.start,
992
1000
  mainAxisSize: MainAxisSize.min,
@@ -1000,9 +1008,7 @@ class _KasyDatePickerState extends State<KasyDatePicker>
1000
1008
  Text(
1001
1009
  widget.label!,
1002
1010
  style: context.textTheme.bodyMedium?.copyWith(
1003
- color: isDisabled
1004
- ? labelColor.withValues(alpha: 0.46)
1005
- : labelColor,
1011
+ color: dimDisabled(labelColor),
1006
1012
  fontWeight: FontWeight.w500,
1007
1013
  ),
1008
1014
  ),
@@ -1066,11 +1072,11 @@ class _KasyDatePickerState extends State<KasyDatePicker>
1066
1072
  child: Text(
1067
1073
  footerText,
1068
1074
  style: context.textTheme.bodySmall?.copyWith(
1069
- color: (hasErrorText ? c.error : c.muted).withValues(
1070
- // Match KasyTextField's disabled description fade (0.34)
1071
- // so the helper text dims along with the field.
1072
- alpha: isDisabled ? 0.34 : 1,
1073
- ),
1075
+ // Soften the helper text via the kit's blend-toward-surface
1076
+ // rule so the field as a whole reads as disabled without any
1077
+ // transparency.
1078
+ color:
1079
+ dimDisabled(hasErrorText ? c.error : c.muted, alpha: 0.45),
1074
1080
  ),
1075
1081
  ),
1076
1082
  ),