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
@@ -231,6 +231,28 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
231
231
  ),
232
232
  ],
233
233
  );
234
+ case 'DatePicker':
235
+ return const ComponentPreviewDefinition(
236
+ title: 'DatePicker',
237
+ variants: [
238
+ ComponentPreviewVariant(
239
+ label: 'Without label',
240
+ builder: _buildDatePickerBasic,
241
+ ),
242
+ ComponentPreviewVariant(
243
+ label: 'With label',
244
+ builder: _buildDatePickerWithLabel,
245
+ ),
246
+ ComponentPreviewVariant(
247
+ label: 'With min & max',
248
+ builder: _buildDatePickerMinMax,
249
+ ),
250
+ ComponentPreviewVariant(
251
+ label: 'Disabled',
252
+ builder: _buildDatePickerDisabled,
253
+ ),
254
+ ],
255
+ );
234
256
  case 'Dialog':
235
257
  return const ComponentPreviewDefinition(
236
258
  title: 'Dialog',
@@ -654,6 +676,72 @@ class _SidebarPreviewState extends State<_SidebarPreview> {
654
676
  }
655
677
 
656
678
 
679
+ // ─────────────────────────────────────────────────────────────────────────────
680
+ // DatePicker — interactive demos
681
+ // ─────────────────────────────────────────────────────────────────────────────
682
+
683
+ Widget _buildDatePickerBasic(BuildContext context) =>
684
+ const _DatePickerPreview(variant: _DatePickerVariant.basic);
685
+
686
+ Widget _buildDatePickerWithLabel(BuildContext context) =>
687
+ const _DatePickerPreview(variant: _DatePickerVariant.withLabel);
688
+
689
+ Widget _buildDatePickerMinMax(BuildContext context) =>
690
+ const _DatePickerPreview(variant: _DatePickerVariant.minMax);
691
+
692
+ Widget _buildDatePickerDisabled(BuildContext context) =>
693
+ const _DatePickerPreview(variant: _DatePickerVariant.disabled);
694
+
695
+ enum _DatePickerVariant { basic, withLabel, minMax, disabled }
696
+
697
+ class _DatePickerPreview extends StatefulWidget {
698
+ const _DatePickerPreview({required this.variant});
699
+
700
+ final _DatePickerVariant variant;
701
+
702
+ @override
703
+ State<_DatePickerPreview> createState() => _DatePickerPreviewState();
704
+ }
705
+
706
+ class _DatePickerPreviewState extends State<_DatePickerPreview> {
707
+ DateTime? _date;
708
+
709
+ @override
710
+ Widget build(BuildContext context) {
711
+ final DateTime today = DateTime.now();
712
+ final DateTime minDate = today.subtract(const Duration(days: 30));
713
+ final DateTime maxDate = today.add(const Duration(days: 60));
714
+
715
+ return Padding(
716
+ padding: const EdgeInsets.all(KasySpacing.md),
717
+ child: switch (widget.variant) {
718
+ _DatePickerVariant.basic => KasyDatePicker(
719
+ value: _date,
720
+ onChanged: (d) => setState(() => _date = d),
721
+ ),
722
+ _DatePickerVariant.withLabel => KasyDatePicker(
723
+ label: 'Date of birth',
724
+ value: _date,
725
+ onChanged: (d) => setState(() => _date = d),
726
+ ),
727
+ _DatePickerVariant.minMax => KasyDatePicker(
728
+ label: 'Appointment date',
729
+ value: _date,
730
+ minDate: minDate,
731
+ maxDate: maxDate,
732
+ onChanged: (d) => setState(() => _date = d),
733
+ ),
734
+ _DatePickerVariant.disabled => KasyDatePicker(
735
+ label: 'Locked date',
736
+ value: today,
737
+ enabled: false,
738
+ onChanged: (d) => setState(() => _date = d),
739
+ ),
740
+ },
741
+ );
742
+ }
743
+ }
744
+
657
745
  // ─────────────────────────────────────────────────────────────────────────────
658
746
  // Tabs — interactive demos
659
747
  // ─────────────────────────────────────────────────────────────────────────────
@@ -667,6 +755,83 @@ Widget _buildTabsSecondaryVariant(BuildContext context) =>
667
755
  Widget _buildTabsFillModeVariant(BuildContext context) =>
668
756
  const _TabsFillModePreview();
669
757
 
758
+ // ── Shared tab content helpers ────────────────────────────────────────────
759
+
760
+ /// Fades [child] in when [tabIndex] == [currentIndex], out otherwise.
761
+ ///
762
+ /// Used inside an [IndexedStack] so the content area never changes height —
763
+ /// the stack always sizes to the tallest child and only the active one is
764
+ /// visible.
765
+ Widget _tabFade(int tabIndex, int currentIndex, Widget child) {
766
+ return AnimatedOpacity(
767
+ opacity: tabIndex == currentIndex ? 1.0 : 0.0,
768
+ duration: const Duration(milliseconds: 180),
769
+ curve: Curves.easeInOut,
770
+ child: child,
771
+ );
772
+ }
773
+
774
+ // ── Card container helper ─────────────────────────────────────────────────
775
+
776
+ Widget _tabsCard(BuildContext context, Widget child) {
777
+ return DecoratedBox(
778
+ decoration: BoxDecoration(
779
+ color: context.colors.surface,
780
+ borderRadius: KasyRadius.lgBorderRadius,
781
+ border: Border.all(
782
+ color: context.colors.outline.withValues(alpha: 0.4),
783
+ ),
784
+ ),
785
+ child: child,
786
+ );
787
+ }
788
+
789
+ // ── Divider helper ────────────────────────────────────────────────────────
790
+
791
+ Widget _tabsDivider(BuildContext context) => Divider(
792
+ height: 1,
793
+ thickness: 1,
794
+ indent: KasySpacing.md,
795
+ endIndent: KasySpacing.md,
796
+ color: context.colors.outline.withValues(alpha: 0.33),
797
+ );
798
+
799
+ // ── Radio-style option row ────────────────────────────────────────────────
800
+
801
+ /// Single-select option row that looks like a radio button using KasyCheckbox.
802
+ class _RadioOptionTile extends StatelessWidget {
803
+ const _RadioOptionTile({
804
+ required this.label,
805
+ required this.description,
806
+ required this.selected,
807
+ required this.onTap,
808
+ });
809
+
810
+ final String label;
811
+ final String description;
812
+ final bool selected;
813
+ final VoidCallback onTap;
814
+
815
+ @override
816
+ Widget build(BuildContext context) {
817
+ return Padding(
818
+ padding: const EdgeInsets.symmetric(
819
+ horizontal: KasySpacing.md,
820
+ vertical: KasySpacing.smd,
821
+ ),
822
+ child: KasyCheckbox(
823
+ value: selected,
824
+ onChanged: (_) => onTap(),
825
+ label: label,
826
+ description: description,
827
+ style: const KasyCheckboxStyle(
828
+ shape: KasyCheckboxShape.circle,
829
+ ),
830
+ ),
831
+ );
832
+ }
833
+ }
834
+
670
835
  // ── Primary variant ────────────────────────────────────────────────────────
671
836
 
672
837
  class _TabsPrimaryPreview extends StatefulWidget {
@@ -679,40 +844,239 @@ class _TabsPrimaryPreview extends StatefulWidget {
679
844
  class _TabsPrimaryPreviewState extends State<_TabsPrimaryPreview> {
680
845
  int _index = 0;
681
846
 
682
- static const List<String> _tabs = ['Overview', 'Details', 'Settings'];
847
+ // General tab
848
+ final TextEditingController _urlController = TextEditingController();
849
+ bool _analytics = true;
850
+ bool _errorReports = false;
683
851
 
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
- ];
852
+ // Appearance tab
853
+ int _theme = 0; // 0=Auto 1=Light 2=Dark
854
+ int _fontSize = 1; // 0=Small 1=Medium 2=Large
855
+
856
+ // Notifications tab
857
+ bool _pushEnabled = true;
858
+ bool _emailEnabled = true;
859
+ bool _mentions = true;
860
+ bool _weeklyDigest = false;
861
+
862
+ // Profile tab
863
+ final TextEditingController _nameController =
864
+ TextEditingController(text: 'Paulo Morales');
865
+ final TextEditingController _usernameController =
866
+ TextEditingController(text: 'paulomorales');
689
867
 
690
868
  @override
691
- Widget build(BuildContext context) {
692
- final KasyColors c = context.colors;
869
+ void dispose() {
870
+ _urlController.dispose();
871
+ _nameController.dispose();
872
+ _usernameController.dispose();
873
+ super.dispose();
874
+ }
875
+
876
+ Widget _generalTab(BuildContext context) {
877
+ return Column(
878
+ crossAxisAlignment: CrossAxisAlignment.stretch,
879
+ mainAxisSize: MainAxisSize.min,
880
+ children: [
881
+ Padding(
882
+ padding: const EdgeInsets.all(KasySpacing.md),
883
+ child: KasyTextField(
884
+ controller: _urlController,
885
+ variant: KasyTextFieldVariant.secondary,
886
+ label: 'Homepage URL',
887
+ hint: 'https://yoursite.com',
888
+ ),
889
+ ),
890
+ _tabsDivider(context),
891
+ KasyCheckboxTile(
892
+ value: _analytics,
893
+ onChanged: (v) => setState(() => _analytics = v),
894
+ label: 'Enable analytics',
895
+ description: 'Collect anonymous usage data to improve the app',
896
+ ),
897
+ _tabsDivider(context),
898
+ KasyCheckboxTile(
899
+ value: _errorReports,
900
+ onChanged: (v) => setState(() => _errorReports = v),
901
+ label: 'Send error reports',
902
+ description: 'Automatically share crash reports with the team',
903
+ ),
904
+ ],
905
+ );
906
+ }
907
+
908
+ Widget _appearanceTab(BuildContext context) {
909
+ return Column(
910
+ crossAxisAlignment: CrossAxisAlignment.stretch,
911
+ mainAxisSize: MainAxisSize.min,
912
+ children: [
913
+ Padding(
914
+ padding: const EdgeInsets.fromLTRB(
915
+ KasySpacing.md,
916
+ KasySpacing.md,
917
+ KasySpacing.md,
918
+ KasySpacing.sm,
919
+ ),
920
+ child: Text(
921
+ 'Theme',
922
+ style: context.textTheme.labelMedium?.copyWith(
923
+ color: context.colors.onSurface,
924
+ fontWeight: FontWeight.w600,
925
+ ),
926
+ ),
927
+ ),
928
+ _RadioOptionTile(
929
+ label: 'Auto',
930
+ description: 'Follows your system appearance',
931
+ selected: _theme == 0,
932
+ onTap: () => setState(() => _theme = 0),
933
+ ),
934
+ _tabsDivider(context),
935
+ _RadioOptionTile(
936
+ label: 'Light',
937
+ description: 'Always use the light theme',
938
+ selected: _theme == 1,
939
+ onTap: () => setState(() => _theme = 1),
940
+ ),
941
+ _tabsDivider(context),
942
+ _RadioOptionTile(
943
+ label: 'Dark',
944
+ description: 'Always use the dark theme',
945
+ selected: _theme == 2,
946
+ onTap: () => setState(() => _theme = 2),
947
+ ),
948
+ _tabsDivider(context),
949
+ Padding(
950
+ padding: const EdgeInsets.fromLTRB(
951
+ KasySpacing.md,
952
+ KasySpacing.md,
953
+ KasySpacing.md,
954
+ KasySpacing.sm,
955
+ ),
956
+ child: Text(
957
+ 'Font size',
958
+ style: context.textTheme.labelMedium?.copyWith(
959
+ color: context.colors.onSurface,
960
+ fontWeight: FontWeight.w600,
961
+ ),
962
+ ),
963
+ ),
964
+ _RadioOptionTile(
965
+ label: 'Small',
966
+ description: 'Compact text for more content on screen',
967
+ selected: _fontSize == 0,
968
+ onTap: () => setState(() => _fontSize = 0),
969
+ ),
970
+ _tabsDivider(context),
971
+ _RadioOptionTile(
972
+ label: 'Medium',
973
+ description: 'Balanced size for most displays',
974
+ selected: _fontSize == 1,
975
+ onTap: () => setState(() => _fontSize = 1),
976
+ ),
977
+ _tabsDivider(context),
978
+ _RadioOptionTile(
979
+ label: 'Large',
980
+ description: 'Larger text for better readability',
981
+ selected: _fontSize == 2,
982
+ onTap: () => setState(() => _fontSize = 2),
983
+ ),
984
+ ],
985
+ );
986
+ }
987
+
988
+ Widget _notificationsTab(BuildContext context) {
989
+ return Column(
990
+ crossAxisAlignment: CrossAxisAlignment.stretch,
991
+ mainAxisSize: MainAxisSize.min,
992
+ children: [
993
+ KasyCheckboxTile(
994
+ value: _pushEnabled,
995
+ onChanged: (v) => setState(() => _pushEnabled = v),
996
+ label: 'Push notifications',
997
+ description: 'Receive alerts directly on your device',
998
+ ),
999
+ _tabsDivider(context),
1000
+ KasyCheckboxTile(
1001
+ value: _emailEnabled,
1002
+ onChanged: (v) => setState(() => _emailEnabled = v),
1003
+ label: 'Email notifications',
1004
+ description: 'Get updates delivered to your inbox',
1005
+ ),
1006
+ _tabsDivider(context),
1007
+ KasyCheckboxTile(
1008
+ value: _mentions,
1009
+ onChanged: (v) => setState(() => _mentions = v),
1010
+ label: 'Mentions and replies',
1011
+ description: 'Notify when someone mentions or replies to you',
1012
+ ),
1013
+ _tabsDivider(context),
1014
+ KasyCheckboxTile(
1015
+ value: _weeklyDigest,
1016
+ onChanged: (v) => setState(() => _weeklyDigest = v),
1017
+ label: 'Weekly digest',
1018
+ description: 'A summary of activity sent every Monday',
1019
+ ),
1020
+ ],
1021
+ );
1022
+ }
1023
+
1024
+ Widget _profileTab(BuildContext context) {
1025
+ return Padding(
1026
+ padding: const EdgeInsets.all(KasySpacing.md),
1027
+ child: Column(
1028
+ crossAxisAlignment: CrossAxisAlignment.stretch,
1029
+ mainAxisSize: MainAxisSize.min,
1030
+ children: [
1031
+ KasyTextField(
1032
+ controller: _nameController,
1033
+ variant: KasyTextFieldVariant.secondary,
1034
+ label: 'Name',
1035
+ hint: 'Your full name',
1036
+ textInputAction: TextInputAction.next,
1037
+ ),
1038
+ const SizedBox(height: KasySpacing.md),
1039
+ KasyTextField(
1040
+ controller: _usernameController,
1041
+ variant: KasyTextFieldVariant.secondary,
1042
+ label: 'Username',
1043
+ hint: 'yourhandle',
1044
+ textInputAction: TextInputAction.done,
1045
+ ),
1046
+ const SizedBox(height: KasySpacing.md),
1047
+ KasyButton(
1048
+ label: 'Update profile',
1049
+ onPressed: () {},
1050
+ ),
1051
+ ],
1052
+ ),
1053
+ );
1054
+ }
693
1055
 
1056
+ @override
1057
+ Widget build(BuildContext context) {
694
1058
  return Column(
695
1059
  crossAxisAlignment: CrossAxisAlignment.stretch,
696
1060
  mainAxisSize: MainAxisSize.min,
697
1061
  children: [
698
1062
  KasyTabs(
699
- tabs: _tabs,
1063
+ tabs: const ['General', 'Appearance', 'Notifications', 'Profile'],
700
1064
  selectedIndex: _index,
701
1065
  onTabSelected: (i) => setState(() => _index = i),
702
1066
  ),
703
1067
  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
- ),
1068
+ // IndexedStack keeps height fixed at the tallest child (Appearance).
1069
+ // AnimatedOpacity on each child provides the smooth crossfade.
1070
+ _tabsCard(
1071
+ context,
1072
+ IndexedStack(
1073
+ index: _index,
1074
+ children: [
1075
+ _tabFade(0, _index, _generalTab(context)),
1076
+ _tabFade(1, _index, _appearanceTab(context)),
1077
+ _tabFade(2, _index, _notificationsTab(context)),
1078
+ _tabFade(3, _index, _profileTab(context)),
1079
+ ],
716
1080
  ),
717
1081
  ),
718
1082
  const SizedBox(height: KasySpacing.lg),
@@ -720,7 +1084,7 @@ class _TabsPrimaryPreviewState extends State<_TabsPrimaryPreview> {
720
1084
  Text(
721
1085
  'WITH DISABLED TAB',
722
1086
  style: context.textTheme.labelSmall?.copyWith(
723
- color: c.muted,
1087
+ color: context.colors.muted,
724
1088
  letterSpacing: 1.2,
725
1089
  fontWeight: FontWeight.w700,
726
1090
  ),
@@ -752,41 +1116,146 @@ class _TabsSecondaryPreview extends StatefulWidget {
752
1116
  class _TabsSecondaryPreviewState extends State<_TabsSecondaryPreview> {
753
1117
  int _index = 0;
754
1118
 
755
- static const List<String> _tabs = ['Inbox', 'Sent', 'Drafts'];
1119
+ // General tab
1120
+ final TextEditingController _siteController = TextEditingController();
1121
+ bool _cacheEnabled = true;
1122
+ bool _debugMode = false;
756
1123
 
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
- ];
1124
+ // Notifications tab
1125
+ bool _pushEnabled = true;
1126
+ bool _emailEnabled = false;
1127
+ bool _weeklyDigest = true;
1128
+
1129
+ // Profile tab
1130
+ final TextEditingController _nameController =
1131
+ TextEditingController(text: 'Paulo Morales');
1132
+ final TextEditingController _emailController =
1133
+ TextEditingController(text: 'paulo@kasy.dev');
762
1134
 
763
1135
  @override
764
- Widget build(BuildContext context) {
765
- final KasyColors c = context.colors;
1136
+ void dispose() {
1137
+ _siteController.dispose();
1138
+ _nameController.dispose();
1139
+ _emailController.dispose();
1140
+ super.dispose();
1141
+ }
766
1142
 
1143
+ Widget _generalTab(BuildContext context) {
1144
+ return Column(
1145
+ crossAxisAlignment: CrossAxisAlignment.stretch,
1146
+ mainAxisSize: MainAxisSize.min,
1147
+ children: [
1148
+ Padding(
1149
+ padding: const EdgeInsets.all(KasySpacing.md),
1150
+ child: KasyTextField(
1151
+ controller: _siteController,
1152
+ variant: KasyTextFieldVariant.secondary,
1153
+ label: 'Site URL',
1154
+ hint: 'https://yoursite.com',
1155
+ ),
1156
+ ),
1157
+ _tabsDivider(context),
1158
+ KasyCheckboxTile(
1159
+ value: _cacheEnabled,
1160
+ onChanged: (v) => setState(() => _cacheEnabled = v),
1161
+ label: 'Enable caching',
1162
+ description: 'Speed up page loads with local cache',
1163
+ ),
1164
+ _tabsDivider(context),
1165
+ KasyCheckboxTile(
1166
+ value: _debugMode,
1167
+ onChanged: (v) => setState(() => _debugMode = v),
1168
+ label: 'Debug mode',
1169
+ description: 'Show detailed logs and error messages',
1170
+ ),
1171
+ ],
1172
+ );
1173
+ }
1174
+
1175
+ Widget _notificationsTab(BuildContext context) {
1176
+ return Column(
1177
+ crossAxisAlignment: CrossAxisAlignment.stretch,
1178
+ mainAxisSize: MainAxisSize.min,
1179
+ children: [
1180
+ KasyCheckboxTile(
1181
+ value: _pushEnabled,
1182
+ onChanged: (v) => setState(() => _pushEnabled = v),
1183
+ label: 'Push notifications',
1184
+ description: 'Receive alerts directly on your device',
1185
+ ),
1186
+ _tabsDivider(context),
1187
+ KasyCheckboxTile(
1188
+ value: _emailEnabled,
1189
+ onChanged: (v) => setState(() => _emailEnabled = v),
1190
+ label: 'Email notifications',
1191
+ description: 'Get updates delivered to your inbox',
1192
+ ),
1193
+ _tabsDivider(context),
1194
+ KasyCheckboxTile(
1195
+ value: _weeklyDigest,
1196
+ onChanged: (v) => setState(() => _weeklyDigest = v),
1197
+ label: 'Weekly digest',
1198
+ description: 'A summary of activity sent every Monday',
1199
+ ),
1200
+ ],
1201
+ );
1202
+ }
1203
+
1204
+ Widget _profileTab(BuildContext context) {
1205
+ return Padding(
1206
+ padding: const EdgeInsets.all(KasySpacing.md),
1207
+ child: Column(
1208
+ crossAxisAlignment: CrossAxisAlignment.stretch,
1209
+ mainAxisSize: MainAxisSize.min,
1210
+ children: [
1211
+ KasyTextField(
1212
+ controller: _nameController,
1213
+ variant: KasyTextFieldVariant.secondary,
1214
+ label: 'Name',
1215
+ hint: 'Your full name',
1216
+ textInputAction: TextInputAction.next,
1217
+ ),
1218
+ const SizedBox(height: KasySpacing.md),
1219
+ KasyTextField(
1220
+ controller: _emailController,
1221
+ variant: KasyTextFieldVariant.secondary,
1222
+ label: 'Email',
1223
+ hint: 'email@example.com',
1224
+ contentType: KasyTextFieldContentType.email,
1225
+ textInputAction: TextInputAction.done,
1226
+ ),
1227
+ const SizedBox(height: KasySpacing.md),
1228
+ KasyButton(
1229
+ label: 'Save changes',
1230
+ onPressed: () {},
1231
+ ),
1232
+ ],
1233
+ ),
1234
+ );
1235
+ }
1236
+
1237
+ @override
1238
+ Widget build(BuildContext context) {
767
1239
  return Column(
768
1240
  crossAxisAlignment: CrossAxisAlignment.stretch,
769
1241
  mainAxisSize: MainAxisSize.min,
770
1242
  children: [
771
1243
  KasyTabs(
772
- tabs: _tabs,
1244
+ tabs: const ['General', 'Notifications', 'Profile'],
773
1245
  selectedIndex: _index,
774
1246
  onTabSelected: (i) => setState(() => _index = i),
775
1247
  variant: KasyTabsVariant.secondary,
776
1248
  ),
777
1249
  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
- ),
1250
+ _tabsCard(
1251
+ context,
1252
+ IndexedStack(
1253
+ index: _index,
1254
+ children: [
1255
+ _tabFade(0, _index, _generalTab(context)),
1256
+ _tabFade(1, _index, _notificationsTab(context)),
1257
+ _tabFade(2, _index, _profileTab(context)),
1258
+ ],
790
1259
  ),
791
1260
  ),
792
1261
  const SizedBox(height: KasySpacing.lg),
@@ -794,7 +1263,7 @@ class _TabsSecondaryPreviewState extends State<_TabsSecondaryPreview> {
794
1263
  Text(
795
1264
  'WITH ICONS',
796
1265
  style: context.textTheme.labelSmall?.copyWith(
797
- color: c.muted,
1266
+ color: context.colors.muted,
798
1267
  letterSpacing: 1.2,
799
1268
  fontWeight: FontWeight.w700,
800
1269
  ),
@@ -826,14 +1295,19 @@ class _TabsFillModePreview extends StatefulWidget {
826
1295
 
827
1296
  class _TabsFillModePreviewState extends State<_TabsFillModePreview> {
828
1297
  int _primaryIndex = 0;
829
- int _secondaryIndex = 0;
1298
+ int _iconIndex = 0;
1299
+
1300
+ static const List<String> _primaryContent = [
1301
+ 'Overview — see a high-level summary of your project status and recent activity.',
1302
+ 'Analytics — track metrics like sessions, retention, and conversion rates over time.',
1303
+ 'Settings — manage project configuration, integrations, and team access.',
1304
+ ];
830
1305
 
831
1306
  @override
832
1307
  Widget build(BuildContext context) {
833
- final KasyColors c = context.colors;
834
- final TextStyle labelStyle =
1308
+ final TextStyle sectionLabel =
835
1309
  context.textTheme.labelSmall?.copyWith(
836
- color: c.muted,
1310
+ color: context.colors.muted,
837
1311
  letterSpacing: 1.2,
838
1312
  fontWeight: FontWeight.w700,
839
1313
  ) ??
@@ -843,38 +1317,52 @@ class _TabsFillModePreviewState extends State<_TabsFillModePreview> {
843
1317
  crossAxisAlignment: CrossAxisAlignment.stretch,
844
1318
  mainAxisSize: MainAxisSize.min,
845
1319
  children: [
846
- Text('PRIMARY + FILL', style: labelStyle),
1320
+ Text('PRIMARY FILL', style: sectionLabel),
847
1321
  const SizedBox(height: KasySpacing.sm),
848
1322
  KasyTabs(
849
- tabs: const ['Tab 1', 'Tab 2', 'Tab 3'],
1323
+ tabs: const ['Overview', 'Analytics', 'Settings'],
850
1324
  selectedIndex: _primaryIndex,
851
1325
  onTabSelected: (i) => setState(() => _primaryIndex = i),
852
1326
  mode: KasyTabsMode.fill,
853
1327
  ),
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
1328
  const SizedBox(height: KasySpacing.md),
865
- AnimatedSwitcher(
866
- duration: const Duration(milliseconds: 200),
867
- child: KasyCard(
868
- key: ValueKey('fill_$_secondaryIndex'),
1329
+ _tabsCard(
1330
+ context,
1331
+ Padding(
869
1332
  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),
1333
+ child: IndexedStack(
1334
+ index: _primaryIndex,
1335
+ children: List.generate(
1336
+ _primaryContent.length,
1337
+ (i) => _tabFade(
1338
+ i,
1339
+ _primaryIndex,
1340
+ Text(
1341
+ _primaryContent[i],
1342
+ style: context.textTheme.bodyMedium?.copyWith(
1343
+ color: context.colors.onSurface.withValues(alpha: 0.75),
1344
+ height: 1.5,
1345
+ ),
1346
+ ),
1347
+ ),
874
1348
  ),
875
1349
  ),
876
1350
  ),
877
1351
  ),
1352
+ const SizedBox(height: KasySpacing.lg),
1353
+ Text('SECONDARY FILL + ICONS', style: sectionLabel),
1354
+ const SizedBox(height: KasySpacing.sm),
1355
+ KasyTabs.items(
1356
+ items: const [
1357
+ KasyTabItem(label: 'Home', icon: KasyIcons.home),
1358
+ KasyTabItem(label: 'Profile', icon: KasyIcons.person),
1359
+ KasyTabItem(label: 'Settings', icon: KasyIcons.settings),
1360
+ ],
1361
+ selectedIndex: _iconIndex,
1362
+ onTabSelected: (i) => setState(() => _iconIndex = i),
1363
+ variant: KasyTabsVariant.secondary,
1364
+ mode: KasyTabsMode.fill,
1365
+ ),
878
1366
  ],
879
1367
  );
880
1368
  }
@@ -1199,7 +1687,7 @@ Widget _buildAvatarGroupVariant(BuildContext context) {
1199
1687
  // Shared gradient avatars list
1200
1688
  final List<Widget> gradients = [
1201
1689
  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),
1690
+ KasyAvatar.gradientFill(size: KasyAvatarSize.small, diameter: d, showShadow: false, gradient: KasyAvatarGradients.sky),
1203
1691
  KasyAvatar.gradientFill(size: KasyAvatarSize.small, diameter: d, showShadow: false, gradient: KasyAvatarGradients.orange),
1204
1692
  KasyAvatar.gradientFill(size: KasyAvatarSize.small, diameter: d, showShadow: false, gradient: KasyAvatarGradients.red),
1205
1693
  KasyAvatar.gradientFill(size: KasyAvatarSize.small, diameter: d, showShadow: false, gradient: KasyAvatarGradients.silver),
@@ -2096,7 +2584,7 @@ Widget _buildBadgeDefault(BuildContext context) {
2096
2584
  child: KasyAvatar.gradientFill(
2097
2585
  size: KasyAvatarSize.medium,
2098
2586
  showShadow: false,
2099
- gradient: KasyAvatarGradients.teal,
2587
+ gradient: KasyAvatarGradients.sky,
2100
2588
  ),
2101
2589
  ),
2102
2590
  const SizedBox(height: KasySpacing.sm),
@@ -4028,9 +4516,9 @@ class _BadgeColorsPreview extends StatelessWidget {
4028
4516
  KasyBadgeTone.warning,
4029
4517
  KasyBadgeTone.danger,
4030
4518
  ];
4031
- final List<Gradient> gradients = [
4519
+ final List<KasyAvatarGradientData> gradients = [
4032
4520
  KasyAvatarGradients.blue,
4033
- KasyAvatarGradients.teal,
4521
+ KasyAvatarGradients.sky,
4034
4522
  KasyAvatarGradients.purple,
4035
4523
  KasyAvatarGradients.orange,
4036
4524
  KasyAvatarGradients.red,
@@ -4153,9 +4641,9 @@ class _BadgePlacementsPreview extends StatelessWidget {
4153
4641
  class _BadgeDotPreview extends StatelessWidget {
4154
4642
  const _BadgeDotPreview();
4155
4643
 
4156
- static const List<(KasyBadgeTone, Gradient)> _items = [
4644
+ static const List<(KasyBadgeTone, KasyAvatarGradientData)> _items = [
4157
4645
  (KasyBadgeTone.neutral, KasyAvatarGradients.blue),
4158
- (KasyBadgeTone.primary, KasyAvatarGradients.teal),
4646
+ (KasyBadgeTone.primary, KasyAvatarGradients.sky),
4159
4647
  (KasyBadgeTone.success, KasyAvatarGradients.purple),
4160
4648
  (KasyBadgeTone.warning, KasyAvatarGradients.red),
4161
4649
  (KasyBadgeTone.danger, KasyAvatarGradients.orange),
@@ -4197,9 +4685,9 @@ class _BadgeVariantsPreview extends StatelessWidget {
4197
4685
  KasyBadgeTone.danger,
4198
4686
  ];
4199
4687
 
4200
- static const List<Gradient> _gradients = [
4688
+ static const List<KasyAvatarGradientData> _gradients = [
4201
4689
  KasyAvatarGradients.blue,
4202
- KasyAvatarGradients.teal,
4690
+ KasyAvatarGradients.sky,
4203
4691
  KasyAvatarGradients.purple,
4204
4692
  KasyAvatarGradients.red,
4205
4693
  KasyAvatarGradients.orange,
@@ -4332,7 +4820,7 @@ class _BadgeWithContentPreview extends StatelessWidget {
4332
4820
  class _BadgeContentItem extends StatelessWidget {
4333
4821
  final String label;
4334
4822
  final KasyBadge badge;
4335
- final Gradient gradient;
4823
+ final KasyAvatarGradientData gradient;
4336
4824
 
4337
4825
  const _BadgeContentItem({
4338
4826
  required this.label,