kasy-cli 1.13.0 → 1.15.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 (157) hide show
  1. package/bin/kasy.js +140 -12
  2. package/lib/commands/add.js +2 -2
  3. package/lib/commands/codemagic.js +11 -4
  4. package/lib/commands/deploy.js +3 -3
  5. package/lib/commands/favicon.js +115 -0
  6. package/lib/commands/icon.js +143 -0
  7. package/lib/commands/ios.js +28 -7
  8. package/lib/commands/new.js +8 -20
  9. package/lib/commands/remove.js +1 -1
  10. package/lib/commands/reset.js +385 -0
  11. package/lib/commands/run.js +24 -17
  12. package/lib/commands/splash.js +14 -4
  13. package/lib/commands/update.js +1 -1
  14. package/lib/scaffold/backends/api/patch/README.md +1 -1
  15. package/lib/scaffold/backends/api/patch/android/app/src/main/AndroidManifest.xml +1 -1
  16. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +53 -0
  17. package/lib/scaffold/backends/api/pubspec.yaml.tpl +11 -1
  18. package/lib/scaffold/backends/firebase/tokens.js +2 -2
  19. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -2
  20. package/lib/scaffold/backends/supabase/migrations/20240101000011_dedupe_device_tokens.sql +34 -0
  21. package/lib/scaffold/backends/supabase/patch/README.md +1 -1
  22. package/lib/scaffold/backends/supabase/patch/android/app/src/main/AndroidManifest.xml +1 -1
  23. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +43 -0
  24. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +11 -1
  25. package/lib/utils/apple-release.js +115 -16
  26. package/lib/utils/checks.js +45 -107
  27. package/lib/utils/debug.js +75 -0
  28. package/lib/utils/flutter-run.js +173 -0
  29. package/lib/utils/friendly-error.js +91 -0
  30. package/lib/utils/i18n/messages-en.js +970 -0
  31. package/lib/utils/i18n/messages-es.js +968 -0
  32. package/lib/utils/i18n/messages-pt.js +968 -0
  33. package/lib/utils/i18n.js +21 -2483
  34. package/lib/utils/mobile-identity.js +35 -0
  35. package/lib/utils/png-padding.js +120 -0
  36. package/lib/utils/ui.js +114 -0
  37. package/package.json +8 -4
  38. package/templates/firebase/README.en.md +1 -1
  39. package/templates/firebase/README.es.md +1 -1
  40. package/templates/firebase/README.md +1 -1
  41. package/templates/firebase/android/app/build.gradle.kts +10 -1
  42. package/templates/firebase/android/app/src/main/AndroidManifest.xml +1 -1
  43. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +25 -1
  44. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +161 -11
  45. package/templates/firebase/android/app/src/main/res/drawable/widget_add_button.xml +15 -0
  46. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +9 -0
  47. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +12 -0
  48. package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +5 -0
  49. package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +17 -0
  50. package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +5 -0
  51. package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
  52. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png +0 -0
  53. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png +0 -0
  54. package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
  55. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png +0 -0
  56. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png +0 -0
  57. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
  58. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
  59. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
  60. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
  61. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
  62. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
  63. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png +0 -0
  64. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png +0 -0
  65. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
  66. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png +0 -0
  67. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png +0 -0
  68. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
  69. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png +0 -0
  70. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png +0 -0
  71. package/templates/firebase/android/app/src/main/res/layout/widget_loading.xml +8 -0
  72. package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +53 -0
  73. package/templates/firebase/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +9 -0
  74. package/templates/firebase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  75. package/templates/firebase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  76. package/templates/firebase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  77. package/templates/firebase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  78. package/templates/firebase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  79. package/templates/firebase/android/app/src/main/res/xml/mywidget_info.xml +9 -3
  80. package/templates/firebase/assets/images/favicon.png +0 -0
  81. package/templates/firebase/assets/images/icon.png +0 -0
  82. package/templates/firebase/assets/images/icon_android.png +0 -0
  83. package/templates/firebase/assets/images/icon_foreground_empty.png +0 -0
  84. package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
  85. package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
  86. package/templates/firebase/firestore.indexes.json +10 -0
  87. package/templates/firebase/functions/src/core/data/entities/user_device_entity.ts +3 -0
  88. package/templates/firebase/functions/src/core/data/repositories/user_device_repository.ts +17 -1
  89. package/templates/firebase/functions/src/index.ts +1 -0
  90. package/templates/firebase/functions/src/notifications/device_triggers.ts +58 -0
  91. package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +116 -33
  92. package/templates/firebase/ios/Runner/AppDelegate.swift +17 -1
  93. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
  94. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
  95. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
  96. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
  97. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
  98. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
  99. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
  100. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
  101. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
  102. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
  103. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png +0 -0
  104. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png +0 -0
  105. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png +0 -0
  106. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png +0 -0
  107. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
  108. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
  109. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png +0 -0
  110. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png +0 -0
  111. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
  112. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
  113. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
  114. package/templates/firebase/ios/Runner/Info.plist +2 -2
  115. package/templates/firebase/ios/Runner/es.lproj/InfoPlist.strings +1 -1
  116. package/templates/firebase/ios/Runner/pt-BR.lproj/InfoPlist.strings +1 -1
  117. package/templates/firebase/ios/Runner/pt.lproj/InfoPlist.strings +1 -1
  118. package/templates/firebase/lib/components/components.dart +1 -0
  119. package/templates/firebase/lib/components/kasy_avatar.dart +88 -57
  120. package/templates/firebase/lib/components/kasy_avatar_presets.dart +116 -74
  121. package/templates/firebase/lib/components/kasy_button.dart +8 -0
  122. package/templates/firebase/lib/components/kasy_tabs.dart +431 -0
  123. package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +18 -0
  124. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +73 -19
  125. package/templates/firebase/lib/core/home_widgets/home_widget_service.dart +22 -8
  126. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +16 -4
  127. package/templates/firebase/lib/core/states/components/maybeshow_component.dart +4 -8
  128. package/templates/firebase/lib/core/states/user_state_notifier.dart +13 -1
  129. package/templates/firebase/lib/features/home/home_components_page.dart +1 -1
  130. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +316 -93
  131. package/templates/firebase/lib/features/home/home_page.dart +0 -6
  132. package/templates/firebase/lib/features/notifications/api/device_api.dart +57 -0
  133. package/templates/firebase/lib/features/notifications/providers/models/notification.dart +11 -1
  134. package/templates/firebase/lib/features/notifications/repositories/device_repository.dart +9 -0
  135. package/templates/firebase/lib/features/notifications/repositories/notifications_repository.dart +1 -4
  136. package/templates/firebase/lib/features/notifications/shared/att_permission.dart +28 -8
  137. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -1
  138. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +44 -11
  139. package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +31 -29
  140. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +21 -5
  141. package/templates/firebase/lib/i18n/en.i18n.json +4 -1
  142. package/templates/firebase/lib/i18n/es.i18n.json +4 -1
  143. package/templates/firebase/lib/i18n/pt.i18n.json +4 -1
  144. package/templates/firebase/pubspec.yaml +10 -3
  145. package/templates/firebase/test/features/notifications/data/device_api_fake.dart +9 -0
  146. package/templates/firebase/web/favicon.png +0 -0
  147. package/templates/firebase/web/icons/Icon-192.png +0 -0
  148. package/templates/firebase/web/icons/Icon-512.png +0 -0
  149. package/templates/firebase/web/icons/Icon-maskable-192.png +0 -0
  150. package/templates/firebase/web/icons/Icon-maskable-512.png +0 -0
  151. package/templates/firebase/web/index.html +9 -0
  152. package/templates/firebase/web/manifest.json +3 -3
  153. package/templates/firebase/assets/images/app_icon.png +0 -0
  154. package/templates/firebase/assets/images/onboarding/img1.jpg +0 -0
  155. package/templates/firebase/assets/images/onboarding/onboarding.png +0 -0
  156. package/templates/firebase/lib/core/states/components/maybe_ask_biometric_setup.dart +0 -88
  157. package/templates/firebase/lib/features/notifications/shared/notification_permission_bottom_sheet.dart +0 -144
@@ -60,11 +60,23 @@ class SharedPreferencesBuilder implements OnStartService {
60
60
  return prefs.getBool('biometric_enabled') ?? false;
61
61
  }
62
62
 
63
- Future<void> setBiometricPromptShown(bool shown) async {
64
- await prefs.setBool('biometric_prompt_shown', shown);
63
+ /// How many times the user dismissed the ATT soft prompt without accepting.
64
+ /// Used to back off and eventually stop asking. See [MaybeShowAttPermission].
65
+ int getAttSoftDismissCount() {
66
+ return prefs.getInt('att_soft_dismiss_count') ?? 0;
65
67
  }
66
68
 
67
- bool getBiometricPromptShown() {
68
- return prefs.getBool('biometric_prompt_shown') ?? false;
69
+ Future<void> setAttSoftDismissCount(int count) async {
70
+ await prefs.setInt('att_soft_dismiss_count', count);
71
+ }
72
+
73
+ DateTime? getAttSoftLastAskedAt() {
74
+ final millis = prefs.getInt('att_soft_last_asked_at');
75
+ if (millis == null) return null;
76
+ return DateTime.fromMillisecondsSinceEpoch(millis);
77
+ }
78
+
79
+ Future<void> setAttSoftLastAskedAt(DateTime when) async {
80
+ await prefs.setInt('att_soft_last_asked_at', when.millisecondsSinceEpoch);
69
81
  }
70
82
  }
@@ -13,20 +13,16 @@ import 'package:kasy_kit/core/states/models/event_model.dart';
13
13
  /// Ex of usage:
14
14
  /// @override
15
15
  /// Widget build(BuildContext context) {
16
- /// final homeState = ref.watch(homeNotifierProvider);
17
- /// final userState = ref.watch(userStateNotifierProvider);
18
- /// // final translations = ref.watch(translationsProvider);
19
- ///
20
16
  /// return ConditionalWidgetsEvents(
21
17
  /// eventWidgets: [
22
18
  /// MaybeShowPremiumPage(),
23
- /// MaybeShowNotificationPermission(),
24
- /// MaybeLevelUpBottomSheet(),
19
+ /// MaybeShowAttPermission(),
25
20
  /// MaybeAskForReview(),
26
21
  /// MaybeAskForRating(),
27
22
  /// ],
28
- /// child: Background.blue(
29
- /// child: SafeArea(...
23
+ /// child: ...,
24
+ /// );
25
+ /// }
30
26
  /// A widget that can be shown or not based on a condition.
31
27
  sealed class MaybeShow {}
32
28
 
@@ -6,6 +6,7 @@ import 'package:kasy_kit/core/data/models/subscription.dart';
6
6
  import 'package:kasy_kit/core/data/models/user.dart';
7
7
  import 'package:kasy_kit/core/data/repositories/user_repository.dart';
8
8
  import 'package:kasy_kit/core/initializer/onstart_service.dart';
9
+ import 'package:kasy_kit/core/shared_preferences/shared_preferences.dart';
9
10
  import 'package:kasy_kit/core/states/models/user_state.dart';
10
11
  import 'package:kasy_kit/environnements.dart';
11
12
  import 'package:kasy_kit/features/authentication/repositories/authentication_repository.dart';
@@ -125,8 +126,19 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
125
126
  Future<void> onLogout() async {
126
127
  final userId = state.user.idOrThrow;
127
128
  _deviceRepository.removeTokenUpdateListener();
128
- await _deviceRepository.unregister(userId);
129
+ // Best-effort: if the network call fails we still proceed with logout so
130
+ // the user is never stuck on the previous account. A stale device doc on
131
+ // the old user is cleaned up server-side by the cross-user token dedup
132
+ // trigger when the same install registers under a new account.
133
+ try {
134
+ await _deviceRepository.unregister(userId);
135
+ } catch (e) {
136
+ _logger.w('Failed to unregister device during logout: $e');
137
+ }
129
138
  await _authenticationRepository.logout();
139
+ // Biometric lock is a per-account preference, not a device-wide one.
140
+ // The next user signing in on this install should start without it set.
141
+ await ref.read(sharedPreferencesProvider).setBiometricEnabled(false);
130
142
  state = const UserState(user: User.anonymous());
131
143
  if (mode == AuthenticationMode.anonymous) {
132
144
  await _loadAnonymousState();
@@ -184,6 +184,7 @@ const Set<String> _kReadyComponents = <String>{
184
184
  'Design System',
185
185
  'Dialog',
186
186
  'Hover',
187
+ 'Tabs',
187
188
  'TextArea',
188
189
  'TextField',
189
190
  'Sidebar',
@@ -218,7 +219,6 @@ const Set<String> _kWebReadyComponents = <String>{
218
219
 
219
220
  const Set<String> _kUrgentComponents = <String>{
220
221
  'Switch',
221
- 'Tabs',
222
222
  'Radio Group',
223
223
  'DatePicker',
224
224
  };
@@ -10,7 +10,7 @@ import 'package:kasy_kit/features/home/home_components_preview_page.dart';
10
10
  /// Previews só instanciam os widgets públicos do kit (`components.dart` +
11
11
  /// tema). Rows / scroll / espaçamentos são apenas layout da vitrine —
12
12
  /// comportamento visual vem das props do componente (ex.: `KasyAccordion.surfaceRadius`,
13
- /// `KasyButton.borderRadius`). Presets opcionais: [KasyAvatarOrbGradients],
13
+ /// `KasyButton.borderRadius`). Presets opcionais: [KasyAvatarGradients],
14
14
  /// [KasyAvatarDemoPhotos] (pacote avatar).
15
15
  class ComponentPreviewDefinition {
16
16
  final String title;
@@ -454,6 +454,24 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
454
454
  ),
455
455
  ],
456
456
  );
457
+ case 'Tabs':
458
+ return const ComponentPreviewDefinition(
459
+ title: 'Tabs',
460
+ variants: [
461
+ ComponentPreviewVariant(
462
+ label: 'Primary variant',
463
+ builder: _buildTabsPrimaryVariant,
464
+ ),
465
+ ComponentPreviewVariant(
466
+ label: 'Secondary variant',
467
+ builder: _buildTabsSecondaryVariant,
468
+ ),
469
+ ComponentPreviewVariant(
470
+ label: 'Fill mode',
471
+ builder: _buildTabsFillModeVariant,
472
+ ),
473
+ ],
474
+ );
457
475
  case 'Sidebar':
458
476
  return const ComponentPreviewDefinition(
459
477
  title: 'Sidebar',
@@ -636,6 +654,232 @@ class _SidebarPreviewState extends State<_SidebarPreview> {
636
654
  }
637
655
 
638
656
 
657
+ // ─────────────────────────────────────────────────────────────────────────────
658
+ // Tabs — interactive demos
659
+ // ─────────────────────────────────────────────────────────────────────────────
660
+
661
+ Widget _buildTabsPrimaryVariant(BuildContext context) =>
662
+ const _TabsPrimaryPreview();
663
+
664
+ Widget _buildTabsSecondaryVariant(BuildContext context) =>
665
+ const _TabsSecondaryPreview();
666
+
667
+ Widget _buildTabsFillModeVariant(BuildContext context) =>
668
+ const _TabsFillModePreview();
669
+
670
+ // ── Primary variant ────────────────────────────────────────────────────────
671
+
672
+ class _TabsPrimaryPreview extends StatefulWidget {
673
+ const _TabsPrimaryPreview();
674
+
675
+ @override
676
+ State<_TabsPrimaryPreview> createState() => _TabsPrimaryPreviewState();
677
+ }
678
+
679
+ class _TabsPrimaryPreviewState extends State<_TabsPrimaryPreview> {
680
+ int _index = 0;
681
+
682
+ static const List<String> _tabs = ['Overview', 'Details', 'Settings'];
683
+
684
+ static const List<String> _content = [
685
+ 'Overview content — a summary of the most important information at a glance.',
686
+ 'Details content — in-depth information about each specific aspect.',
687
+ 'Settings content — configure preferences and options here.',
688
+ ];
689
+
690
+ @override
691
+ Widget build(BuildContext context) {
692
+ final KasyColors c = context.colors;
693
+
694
+ return Column(
695
+ crossAxisAlignment: CrossAxisAlignment.stretch,
696
+ mainAxisSize: MainAxisSize.min,
697
+ children: [
698
+ KasyTabs(
699
+ tabs: _tabs,
700
+ selectedIndex: _index,
701
+ onTabSelected: (i) => setState(() => _index = i),
702
+ ),
703
+ const SizedBox(height: KasySpacing.md),
704
+ AnimatedSwitcher(
705
+ duration: const Duration(milliseconds: 200),
706
+ child: KasyCard(
707
+ key: ValueKey(_index),
708
+ padding: const EdgeInsets.all(KasySpacing.md),
709
+ child: Text(
710
+ _content[_index],
711
+ style: context.textTheme.bodyMedium?.copyWith(
712
+ color: c.onSurface.withValues(alpha: 0.75),
713
+ height: 1.5,
714
+ ),
715
+ ),
716
+ ),
717
+ ),
718
+ const SizedBox(height: KasySpacing.lg),
719
+ // Disabled tab demo
720
+ Text(
721
+ 'WITH DISABLED TAB',
722
+ style: context.textTheme.labelSmall?.copyWith(
723
+ color: c.muted,
724
+ letterSpacing: 1.2,
725
+ fontWeight: FontWeight.w700,
726
+ ),
727
+ ),
728
+ const SizedBox(height: KasySpacing.sm),
729
+ KasyTabs.items(
730
+ items: const [
731
+ KasyTabItem(label: 'Active'),
732
+ KasyTabItem(label: 'Disabled', enabled: false),
733
+ KasyTabItem(label: 'Active'),
734
+ ],
735
+ selectedIndex: 0,
736
+ onTabSelected: (_) {},
737
+ ),
738
+ ],
739
+ );
740
+ }
741
+ }
742
+
743
+ // ── Secondary variant ──────────────────────────────────────────────────────
744
+
745
+ class _TabsSecondaryPreview extends StatefulWidget {
746
+ const _TabsSecondaryPreview();
747
+
748
+ @override
749
+ State<_TabsSecondaryPreview> createState() => _TabsSecondaryPreviewState();
750
+ }
751
+
752
+ class _TabsSecondaryPreviewState extends State<_TabsSecondaryPreview> {
753
+ int _index = 0;
754
+
755
+ static const List<String> _tabs = ['Inbox', 'Sent', 'Drafts'];
756
+
757
+ static const List<String> _content = [
758
+ 'Inbox — your incoming messages appear here.',
759
+ 'Sent — messages you have already sent.',
760
+ 'Drafts — unfinished messages saved for later.',
761
+ ];
762
+
763
+ @override
764
+ Widget build(BuildContext context) {
765
+ final KasyColors c = context.colors;
766
+
767
+ return Column(
768
+ crossAxisAlignment: CrossAxisAlignment.stretch,
769
+ mainAxisSize: MainAxisSize.min,
770
+ children: [
771
+ KasyTabs(
772
+ tabs: _tabs,
773
+ selectedIndex: _index,
774
+ onTabSelected: (i) => setState(() => _index = i),
775
+ variant: KasyTabsVariant.secondary,
776
+ ),
777
+ const SizedBox(height: KasySpacing.md),
778
+ AnimatedSwitcher(
779
+ duration: const Duration(milliseconds: 200),
780
+ child: KasyCard(
781
+ key: ValueKey(_index),
782
+ padding: const EdgeInsets.all(KasySpacing.md),
783
+ child: Text(
784
+ _content[_index],
785
+ style: context.textTheme.bodyMedium?.copyWith(
786
+ color: c.onSurface.withValues(alpha: 0.75),
787
+ height: 1.5,
788
+ ),
789
+ ),
790
+ ),
791
+ ),
792
+ const SizedBox(height: KasySpacing.lg),
793
+ // With icons
794
+ Text(
795
+ 'WITH ICONS',
796
+ style: context.textTheme.labelSmall?.copyWith(
797
+ color: c.muted,
798
+ letterSpacing: 1.2,
799
+ fontWeight: FontWeight.w700,
800
+ ),
801
+ ),
802
+ const SizedBox(height: KasySpacing.sm),
803
+ KasyTabs.items(
804
+ items: const [
805
+ KasyTabItem(label: 'Home', icon: KasyIcons.home),
806
+ KasyTabItem(label: 'Profile', icon: KasyIcons.person),
807
+ KasyTabItem(label: 'Settings', icon: KasyIcons.settings),
808
+ ],
809
+ selectedIndex: 1,
810
+ onTabSelected: (_) {},
811
+ variant: KasyTabsVariant.secondary,
812
+ ),
813
+ ],
814
+ );
815
+ }
816
+ }
817
+
818
+ // ── Fill mode ──────────────────────────────────────────────────────────────
819
+
820
+ class _TabsFillModePreview extends StatefulWidget {
821
+ const _TabsFillModePreview();
822
+
823
+ @override
824
+ State<_TabsFillModePreview> createState() => _TabsFillModePreviewState();
825
+ }
826
+
827
+ class _TabsFillModePreviewState extends State<_TabsFillModePreview> {
828
+ int _primaryIndex = 0;
829
+ int _secondaryIndex = 0;
830
+
831
+ @override
832
+ Widget build(BuildContext context) {
833
+ final KasyColors c = context.colors;
834
+ final TextStyle labelStyle =
835
+ context.textTheme.labelSmall?.copyWith(
836
+ color: c.muted,
837
+ letterSpacing: 1.2,
838
+ fontWeight: FontWeight.w700,
839
+ ) ??
840
+ const TextStyle();
841
+
842
+ return Column(
843
+ crossAxisAlignment: CrossAxisAlignment.stretch,
844
+ mainAxisSize: MainAxisSize.min,
845
+ children: [
846
+ Text('PRIMARY + FILL', style: labelStyle),
847
+ const SizedBox(height: KasySpacing.sm),
848
+ KasyTabs(
849
+ tabs: const ['Tab 1', 'Tab 2', 'Tab 3'],
850
+ selectedIndex: _primaryIndex,
851
+ onTabSelected: (i) => setState(() => _primaryIndex = i),
852
+ mode: KasyTabsMode.fill,
853
+ ),
854
+ const SizedBox(height: KasySpacing.lg),
855
+ Text('SECONDARY + FILL', style: labelStyle),
856
+ const SizedBox(height: KasySpacing.sm),
857
+ KasyTabs(
858
+ tabs: const ['Tab 1', 'Tab 2', 'Tab 3'],
859
+ selectedIndex: _secondaryIndex,
860
+ onTabSelected: (i) => setState(() => _secondaryIndex = i),
861
+ variant: KasyTabsVariant.secondary,
862
+ mode: KasyTabsMode.fill,
863
+ ),
864
+ const SizedBox(height: KasySpacing.md),
865
+ AnimatedSwitcher(
866
+ duration: const Duration(milliseconds: 200),
867
+ child: KasyCard(
868
+ key: ValueKey('fill_$_secondaryIndex'),
869
+ padding: const EdgeInsets.all(KasySpacing.md),
870
+ child: Text(
871
+ 'Content for Tab ${_secondaryIndex + 1}',
872
+ style: context.textTheme.bodyMedium?.copyWith(
873
+ color: c.onSurface.withValues(alpha: 0.75),
874
+ ),
875
+ ),
876
+ ),
877
+ ),
878
+ ],
879
+ );
880
+ }
881
+ }
882
+
639
883
  Widget _buildSkeletonCardVariant(BuildContext context) {
640
884
  return const _SkeletonCardPreview();
641
885
  }
@@ -803,7 +1047,7 @@ Widget _buildAvatarSizesVariant(BuildContext context) {
803
1047
  children: [
804
1048
  KasyAvatar.gradientFill(
805
1049
  size: KasyAvatarSize.small,
806
- gradient: KasyAvatarOrbGradients.sphereBlue,
1050
+ gradient: KasyAvatarGradients.blue,
807
1051
  onTap: _triggerTapFeedback,
808
1052
  ),
809
1053
  const SizedBox(height: KasySpacing.sm),
@@ -822,7 +1066,7 @@ Widget _buildAvatarSizesVariant(BuildContext context) {
822
1066
  children: [
823
1067
  KasyAvatar.gradientFill(
824
1068
  size: KasyAvatarSize.medium,
825
- gradient: KasyAvatarOrbGradients.sphereFuchsia,
1069
+ gradient: KasyAvatarGradients.hotPink,
826
1070
  onTap: _triggerTapFeedback,
827
1071
  ),
828
1072
  const SizedBox(height: KasySpacing.sm),
@@ -841,7 +1085,7 @@ Widget _buildAvatarSizesVariant(BuildContext context) {
841
1085
  children: [
842
1086
  KasyAvatar.gradientFill(
843
1087
  size: KasyAvatarSize.large,
844
- gradient: KasyAvatarOrbGradients.sphereSunset,
1088
+ gradient: KasyAvatarGradients.red,
845
1089
  onTap: _triggerTapFeedback,
846
1090
  ),
847
1091
  const SizedBox(height: KasySpacing.sm),
@@ -926,7 +1170,7 @@ Widget _buildAvatarCustomFallbackVariant(BuildContext context) {
926
1170
  const SizedBox(width: KasySpacing.md),
927
1171
  const KasyAvatar(
928
1172
  initials: 'GB',
929
- backgroundGradient: KasyAvatarOrbGradients.sphereGb,
1173
+ backgroundGradient: KasyAvatarGradients.forest,
930
1174
  onTap: _triggerTapFeedback,
931
1175
  ),
932
1176
  const SizedBox(width: KasySpacing.md),
@@ -942,83 +1186,65 @@ Widget _buildAvatarCustomFallbackVariant(BuildContext context) {
942
1186
 
943
1187
  Widget _buildAvatarGroupVariant(BuildContext context) {
944
1188
  const double d = 48;
1189
+
1190
+ Widget label(String text) => Text(
1191
+ text,
1192
+ style: context.textTheme.labelSmall?.copyWith(
1193
+ color: context.colors.muted,
1194
+ letterSpacing: 1.2,
1195
+ fontWeight: FontWeight.w700,
1196
+ ),
1197
+ );
1198
+
1199
+ // Shared gradient avatars list
1200
+ final List<Widget> gradients = [
1201
+ KasyAvatar.gradientFill(size: KasyAvatarSize.small, diameter: d, showShadow: false, gradient: KasyAvatarGradients.blue),
1202
+ KasyAvatar.gradientFill(size: KasyAvatarSize.small, diameter: d, showShadow: false, gradient: KasyAvatarGradients.teal),
1203
+ KasyAvatar.gradientFill(size: KasyAvatarSize.small, diameter: d, showShadow: false, gradient: KasyAvatarGradients.orange),
1204
+ KasyAvatar.gradientFill(size: KasyAvatarSize.small, diameter: d, showShadow: false, gradient: KasyAvatarGradients.red),
1205
+ KasyAvatar.gradientFill(size: KasyAvatarSize.small, diameter: d, showShadow: false, gradient: KasyAvatarGradients.silver),
1206
+ ];
1207
+
1208
+ // Shared photo avatars list
1209
+ const List<Widget> photos = [
1210
+ KasyAvatar(diameter: d, image: NetworkImage(KasyAvatarDemoPhotos.manGlasses)),
1211
+ KasyAvatar(diameter: d, image: NetworkImage(KasyAvatarDemoPhotos.womanSmile)),
1212
+ KasyAvatar(diameter: d, image: NetworkImage(KasyAvatarDemoPhotos.womanCurly)),
1213
+ KasyAvatar(diameter: d, image: NetworkImage(KasyAvatarDemoPhotos.manBeard)),
1214
+ KasyAvatar(diameter: d, image: NetworkImage(KasyAvatarDemoPhotos.manGlasses)),
1215
+ ];
1216
+
945
1217
  return Column(
946
1218
  mainAxisSize: MainAxisSize.min,
947
1219
  crossAxisAlignment: CrossAxisAlignment.stretch,
948
1220
  children: [
949
- Text(
950
- 'BASIC',
951
- style: context.textTheme.labelSmall?.copyWith(
952
- color: context.colors.muted,
953
- letterSpacing: 1.2,
954
- fontWeight: FontWeight.w700,
955
- ),
956
- ),
1221
+ label('GRADIENTS + ADD'),
957
1222
  const SizedBox(height: KasySpacing.sm),
958
1223
  Center(
959
1224
  child: KasyAvatarGroup(
960
- avatars: [
961
- KasyAvatar.gradientFill(
962
- size: KasyAvatarSize.small,
963
- diameter: d,
964
- showShadow: false,
965
- gradient: KasyAvatarOrbGradients.sphereBlue,
966
- ),
967
- KasyAvatar.gradientFill(
968
- size: KasyAvatarSize.small,
969
- diameter: d,
970
- showShadow: false,
971
- gradient: KasyAvatarOrbGradients.sphereTeal,
972
- ),
973
- KasyAvatar.gradientFill(
974
- size: KasyAvatarSize.small,
975
- diameter: d,
976
- showShadow: false,
977
- gradient: KasyAvatarOrbGradients.sphereViolet,
978
- ),
979
- KasyAvatar.gradientFill(
980
- size: KasyAvatarSize.small,
981
- diameter: d,
982
- showShadow: false,
983
- gradient: KasyAvatarOrbGradients.sphereAmber,
984
- ),
985
- ],
1225
+ maxVisible: 5,
1226
+ extraCount: 5,
1227
+ avatars: gradients,
1228
+ onAdd: () {},
986
1229
  ),
987
1230
  ),
988
1231
  const SizedBox(height: KasySpacing.lg),
989
- Text(
990
- 'WITH OVERFLOW',
991
- style: context.textTheme.labelSmall?.copyWith(
992
- color: context.colors.muted,
993
- letterSpacing: 1.2,
994
- fontWeight: FontWeight.w700,
1232
+ label('PHOTOS + ADD'),
1233
+ const SizedBox(height: KasySpacing.sm),
1234
+ Center(
1235
+ child: KasyAvatarGroup(
1236
+ maxVisible: 5,
1237
+ extraCount: 5,
1238
+ avatars: photos,
1239
+ onAdd: () {},
995
1240
  ),
996
1241
  ),
1242
+ const SizedBox(height: KasySpacing.lg),
1243
+ label('WITHOUT ADD'),
997
1244
  const SizedBox(height: KasySpacing.sm),
998
1245
  Center(
999
1246
  child: KasyAvatarGroup(
1000
- maxVisible: 3,
1001
- extraCount: 2,
1002
- avatars: [
1003
- KasyAvatar.gradientFill(
1004
- size: KasyAvatarSize.small,
1005
- diameter: d,
1006
- showShadow: false,
1007
- gradient: KasyAvatarOrbGradients.sphereBlue,
1008
- ),
1009
- KasyAvatar.gradientFill(
1010
- size: KasyAvatarSize.small,
1011
- diameter: d,
1012
- showShadow: false,
1013
- gradient: KasyAvatarOrbGradients.sphereTeal,
1014
- ),
1015
- KasyAvatar.gradientFill(
1016
- size: KasyAvatarSize.small,
1017
- diameter: d,
1018
- showShadow: false,
1019
- gradient: KasyAvatarOrbGradients.sphereViolet,
1020
- ),
1021
- ],
1247
+ avatars: gradients,
1022
1248
  ),
1023
1249
  ),
1024
1250
  ],
@@ -1834,7 +2060,7 @@ Widget _buildBadgeDefault(BuildContext context) {
1834
2060
  child: KasyAvatar.gradientFill(
1835
2061
  size: KasyAvatarSize.medium,
1836
2062
  showShadow: false,
1837
- gradient: KasyAvatarOrbGradients.sphereBlue,
2063
+ gradient: KasyAvatarGradients.blue,
1838
2064
  ),
1839
2065
  ),
1840
2066
  const SizedBox(height: KasySpacing.sm),
@@ -1850,7 +2076,7 @@ Widget _buildBadgeDefault(BuildContext context) {
1850
2076
  child: KasyAvatar.gradientFill(
1851
2077
  size: KasyAvatarSize.medium,
1852
2078
  showShadow: false,
1853
- gradient: KasyAvatarOrbGradients.sphereFuchsia,
2079
+ gradient: KasyAvatarGradients.hotPink,
1854
2080
  ),
1855
2081
  ),
1856
2082
  const SizedBox(height: KasySpacing.sm),
@@ -1870,7 +2096,7 @@ Widget _buildBadgeDefault(BuildContext context) {
1870
2096
  child: KasyAvatar.gradientFill(
1871
2097
  size: KasyAvatarSize.medium,
1872
2098
  showShadow: false,
1873
- gradient: KasyAvatarOrbGradients.sphereTeal,
2099
+ gradient: KasyAvatarGradients.teal,
1874
2100
  ),
1875
2101
  ),
1876
2102
  const SizedBox(height: KasySpacing.sm),
@@ -3803,11 +4029,11 @@ class _BadgeColorsPreview extends StatelessWidget {
3803
4029
  KasyBadgeTone.danger,
3804
4030
  ];
3805
4031
  final List<Gradient> gradients = [
3806
- KasyAvatarOrbGradients.sphereBlue,
3807
- KasyAvatarOrbGradients.sphereTeal,
3808
- KasyAvatarOrbGradients.sphereViolet,
3809
- KasyAvatarOrbGradients.sphereAmber,
3810
- KasyAvatarOrbGradients.sphereSunset,
4032
+ KasyAvatarGradients.blue,
4033
+ KasyAvatarGradients.teal,
4034
+ KasyAvatarGradients.purple,
4035
+ KasyAvatarGradients.orange,
4036
+ KasyAvatarGradients.red,
3811
4037
  ];
3812
4038
  return Center(
3813
4039
  child: Row(
@@ -3871,7 +4097,7 @@ class _BadgeSizeItem extends StatelessWidget {
3871
4097
  child: KasyAvatar.gradientFill(
3872
4098
  size: avatarSize,
3873
4099
  showShadow: false,
3874
- gradient: KasyAvatarOrbGradients.sphereSunset,
4100
+ gradient: KasyAvatarGradients.red,
3875
4101
  ),
3876
4102
  ),
3877
4103
  const SizedBox(height: KasySpacing.sm),
@@ -3910,7 +4136,7 @@ class _BadgePlacementsPreview extends StatelessWidget {
3910
4136
  child: KasyAvatar.gradientFill(
3911
4137
  size: KasyAvatarSize.medium,
3912
4138
  showShadow: false,
3913
- gradient: KasyAvatarOrbGradients.sphereViolet,
4139
+ gradient: KasyAvatarGradients.purple,
3914
4140
  ),
3915
4141
  ),
3916
4142
  const SizedBox(height: KasySpacing.sm),
@@ -3928,11 +4154,11 @@ class _BadgeDotPreview extends StatelessWidget {
3928
4154
  const _BadgeDotPreview();
3929
4155
 
3930
4156
  static const List<(KasyBadgeTone, Gradient)> _items = [
3931
- (KasyBadgeTone.neutral, KasyAvatarOrbGradients.sphereBlue),
3932
- (KasyBadgeTone.primary, KasyAvatarOrbGradients.sphereTeal),
3933
- (KasyBadgeTone.success, KasyAvatarOrbGradients.sphereViolet),
3934
- (KasyBadgeTone.warning, KasyAvatarOrbGradients.sphereSunset),
3935
- (KasyBadgeTone.danger, KasyAvatarOrbGradients.sphereAmber),
4157
+ (KasyBadgeTone.neutral, KasyAvatarGradients.blue),
4158
+ (KasyBadgeTone.primary, KasyAvatarGradients.teal),
4159
+ (KasyBadgeTone.success, KasyAvatarGradients.purple),
4160
+ (KasyBadgeTone.warning, KasyAvatarGradients.red),
4161
+ (KasyBadgeTone.danger, KasyAvatarGradients.orange),
3936
4162
  ];
3937
4163
 
3938
4164
  @override
@@ -3972,11 +4198,11 @@ class _BadgeVariantsPreview extends StatelessWidget {
3972
4198
  ];
3973
4199
 
3974
4200
  static const List<Gradient> _gradients = [
3975
- KasyAvatarOrbGradients.sphereBlue,
3976
- KasyAvatarOrbGradients.sphereTeal,
3977
- KasyAvatarOrbGradients.sphereViolet,
3978
- KasyAvatarOrbGradients.sphereSunset,
3979
- KasyAvatarOrbGradients.sphereAmber,
4201
+ KasyAvatarGradients.blue,
4202
+ KasyAvatarGradients.teal,
4203
+ KasyAvatarGradients.purple,
4204
+ KasyAvatarGradients.red,
4205
+ KasyAvatarGradients.orange,
3980
4206
  ];
3981
4207
 
3982
4208
  static const List<KasyAvatarTone> _avatarTones = [
@@ -4044,7 +4270,6 @@ class _BadgeVariantsPreview extends StatelessWidget {
4044
4270
  child: KasyAvatar(
4045
4271
  initials: 'JD',
4046
4272
  tone: _avatarTones[i],
4047
- showShadow: false,
4048
4273
  ),
4049
4274
  ),
4050
4275
  );
@@ -4064,7 +4289,6 @@ class _BadgeVariantsPreview extends StatelessWidget {
4064
4289
  initials: 'JD',
4065
4290
  tone: _avatarTones[i],
4066
4291
  fallbackSurface: KasyAvatarFallbackSurface.soft,
4067
- showShadow: false,
4068
4292
  ),
4069
4293
  ),
4070
4294
  );
@@ -4085,19 +4309,19 @@ class _BadgeWithContentPreview extends StatelessWidget {
4085
4309
  _BadgeContentItem(
4086
4310
  label: 'Number',
4087
4311
  badge: KasyBadge(text: '5'),
4088
- gradient: KasyAvatarOrbGradients.sphereAmber,
4312
+ gradient: KasyAvatarGradients.orange,
4089
4313
  ),
4090
4314
  SizedBox(width: KasySpacing.lg),
4091
4315
  _BadgeContentItem(
4092
4316
  label: 'Text',
4093
4317
  badge: KasyBadge(text: 'New', tone: KasyBadgeTone.primary),
4094
- gradient: KasyAvatarOrbGradients.sphereBlue,
4318
+ gradient: KasyAvatarGradients.blue,
4095
4319
  ),
4096
4320
  SizedBox(width: KasySpacing.lg),
4097
4321
  _BadgeContentItem(
4098
4322
  label: 'Overflow',
4099
4323
  badge: KasyBadge(text: '99+'),
4100
- gradient: KasyAvatarOrbGradients.sphereViolet,
4324
+ gradient: KasyAvatarGradients.purple,
4101
4325
  ),
4102
4326
  ],
4103
4327
  ),
@@ -6727,7 +6951,6 @@ class _RealCardContent extends StatelessWidget {
6727
6951
  const KasyAvatar(
6728
6952
  size: KasyAvatarSize.small,
6729
6953
  initials: 'AJ',
6730
- showShadow: false,
6731
6954
  ),
6732
6955
  const SizedBox(width: KasySpacing.smd),
6733
6956
  Expanded(