kasy-cli 1.17.0 → 1.19.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 (110) hide show
  1. package/bin/kasy.js +16 -2
  2. package/lib/commands/add.js +7 -7
  3. package/lib/commands/configure.js +548 -0
  4. package/lib/commands/deploy.js +4 -4
  5. package/lib/commands/doctor.js +17 -0
  6. package/lib/commands/favicon.js +4 -4
  7. package/lib/commands/icon.js +5 -5
  8. package/lib/commands/new.js +483 -324
  9. package/lib/commands/run.js +17 -4
  10. package/lib/commands/splash.js +5 -5
  11. package/lib/commands/update.js +9 -9
  12. package/lib/scaffold/CHANGELOG.json +14 -0
  13. package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +108 -0
  14. package/lib/scaffold/backends/firebase/setup-from-scratch.js +123 -5
  15. package/lib/scaffold/generate.js +24 -8
  16. package/lib/scaffold/shared/post-build.js +8 -0
  17. package/lib/utils/brand.js +16 -12
  18. package/lib/utils/flutter-run.js +139 -11
  19. package/lib/utils/i18n/messages-en.js +62 -5
  20. package/lib/utils/i18n/messages-es.js +62 -5
  21. package/lib/utils/i18n/messages-pt.js +63 -6
  22. package/lib/utils/ui.js +79 -4
  23. package/package.json +1 -2
  24. package/templates/firebase/README.en.md +1 -1
  25. package/templates/firebase/README.es.md +1 -1
  26. package/templates/firebase/README.md +1 -1
  27. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +0 -15
  28. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +65 -26
  29. package/templates/firebase/android/app/src/main/res/drawable/widget_add_button.xml +2 -2
  30. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +7 -2
  31. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +6 -6
  32. package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +1 -1
  33. package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +2 -2
  34. package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +1 -1
  35. package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
  36. package/templates/firebase/android/app/src/main/res/drawable-hdpi/splash.png +0 -0
  37. package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
  38. package/templates/firebase/android/app/src/main/res/drawable-mdpi/splash.png +0 -0
  39. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
  40. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/splash.png +0 -0
  41. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
  42. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/splash.png +0 -0
  43. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
  44. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/splash.png +0 -0
  45. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
  46. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/splash.png +0 -0
  47. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
  48. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/splash.png +0 -0
  49. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
  50. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/splash.png +0 -0
  51. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
  52. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/splash.png +0 -0
  53. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
  54. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/splash.png +0 -0
  55. package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +3 -3
  56. package/templates/firebase/android/app/src/main/res/values/colors.xml +32 -0
  57. package/templates/firebase/assets/images/splash_logo_dark.png +0 -0
  58. package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
  59. package/templates/firebase/assets/images/splash_logo_light.png +0 -0
  60. package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
  61. package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +75 -29
  62. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
  63. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
  64. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
  65. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png +0 -0
  66. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png +0 -0
  67. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png +0 -0
  68. package/templates/firebase/ios/Runner/Base.lproj/LaunchScreen.storyboard +1 -1
  69. package/templates/firebase/lib/components/components.dart +1 -0
  70. package/templates/firebase/lib/components/kasy_avatar.dart +65 -17
  71. package/templates/firebase/lib/components/kasy_avatar_presets.dart +121 -97
  72. package/templates/firebase/lib/components/kasy_button.dart +8 -8
  73. package/templates/firebase/lib/components/kasy_date_picker.dart +2173 -0
  74. package/templates/firebase/lib/components/kasy_tabs.dart +214 -91
  75. package/templates/firebase/lib/components/kasy_text_area.dart +9 -4
  76. package/templates/firebase/lib/components/kasy_text_field.dart +96 -36
  77. package/templates/firebase/lib/components/kasy_text_field_otp.dart +1 -2
  78. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +88 -35
  79. package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +7 -43
  80. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +118 -16
  81. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +14 -20
  82. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +12 -18
  83. package/templates/firebase/lib/core/security/secured_storage.dart +56 -15
  84. package/templates/firebase/lib/core/theme/providers/theme_provider.dart +3 -0
  85. package/templates/firebase/lib/core/theme/web_background_sync.dart +3 -0
  86. package/templates/firebase/lib/core/theme/web_background_sync_web.dart +18 -0
  87. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +3 -6
  88. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +6 -0
  89. package/templates/firebase/lib/features/home/home_components_page.dart +3 -2
  90. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +949 -77
  91. package/templates/firebase/lib/features/home/home_page.dart +17 -40
  92. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +1 -16
  93. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +0 -4
  94. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +10 -5
  95. package/templates/firebase/lib/i18n/en.i18n.json +2 -1
  96. package/templates/firebase/lib/i18n/es.i18n.json +2 -1
  97. package/templates/firebase/lib/i18n/pt.i18n.json +2 -1
  98. package/templates/firebase/lib/main.dart +34 -34
  99. package/templates/firebase/pubspec.yaml +2 -1
  100. package/templates/firebase/storage.cors.json +8 -0
  101. package/templates/firebase/web/index.html +24 -2
  102. package/templates/firebase/web/splash/img/dark-1x.png +0 -0
  103. package/templates/firebase/web/splash/img/dark-2x.png +0 -0
  104. package/templates/firebase/web/splash/img/dark-3x.png +0 -0
  105. package/templates/firebase/web/splash/img/dark-4x.png +0 -0
  106. package/templates/firebase/web/splash/img/light-1x.png +0 -0
  107. package/templates/firebase/web/splash/img/light-2x.png +0 -0
  108. package/templates/firebase/web/splash/img/light-3x.png +0 -0
  109. package/templates/firebase/web/splash/img/light-4x.png +0 -0
  110. package/templates/firebase/lib/core/bottom_menu/kasy_bart_navigation.dart +0 -22
@@ -231,6 +231,36 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
231
231
  ),
232
232
  ],
233
233
  );
234
+ case 'DatePicker':
235
+ return const ComponentPreviewDefinition(
236
+ title: 'DatePicker',
237
+ variants: [
238
+ ComponentPreviewVariant(
239
+ label: 'Presentations',
240
+ builder: _buildDatePickerPresentations,
241
+ ),
242
+ ComponentPreviewVariant(
243
+ label: 'Range selection',
244
+ builder: _buildDatePickerRange,
245
+ ),
246
+ ComponentPreviewVariant(
247
+ label: 'Multi-month',
248
+ builder: _buildDatePickerMultiMonth,
249
+ ),
250
+ ComponentPreviewVariant(
251
+ label: 'Date formats',
252
+ builder: _buildDatePickerFormats,
253
+ ),
254
+ ComponentPreviewVariant(
255
+ label: 'Variants',
256
+ builder: _buildDatePickerVariants,
257
+ ),
258
+ ComponentPreviewVariant(
259
+ label: 'Field states',
260
+ builder: _buildDatePickerFieldStates,
261
+ ),
262
+ ],
263
+ );
234
264
  case 'Dialog':
235
265
  return const ComponentPreviewDefinition(
236
266
  title: 'Dialog',
@@ -654,6 +684,7 @@ class _SidebarPreviewState extends State<_SidebarPreview> {
654
684
  }
655
685
 
656
686
 
687
+ // ─────────────────────────────────────────────────────────────────────────────
657
688
  // ─────────────────────────────────────────────────────────────────────────────
658
689
  // Tabs — interactive demos
659
690
  // ─────────────────────────────────────────────────────────────────────────────
@@ -667,6 +698,83 @@ Widget _buildTabsSecondaryVariant(BuildContext context) =>
667
698
  Widget _buildTabsFillModeVariant(BuildContext context) =>
668
699
  const _TabsFillModePreview();
669
700
 
701
+ // ── Shared tab content helpers ────────────────────────────────────────────
702
+
703
+ /// Fades [child] in when [tabIndex] == [currentIndex], out otherwise.
704
+ ///
705
+ /// Used inside an [IndexedStack] so the content area never changes height —
706
+ /// the stack always sizes to the tallest child and only the active one is
707
+ /// visible.
708
+ Widget _tabFade(int tabIndex, int currentIndex, Widget child) {
709
+ return AnimatedOpacity(
710
+ opacity: tabIndex == currentIndex ? 1.0 : 0.0,
711
+ duration: const Duration(milliseconds: 180),
712
+ curve: Curves.easeInOut,
713
+ child: child,
714
+ );
715
+ }
716
+
717
+ // ── Card container helper ─────────────────────────────────────────────────
718
+
719
+ Widget _tabsCard(BuildContext context, Widget child) {
720
+ return DecoratedBox(
721
+ decoration: BoxDecoration(
722
+ color: context.colors.surface,
723
+ borderRadius: KasyRadius.lgBorderRadius,
724
+ border: Border.all(
725
+ color: context.colors.outline.withValues(alpha: 0.4),
726
+ ),
727
+ ),
728
+ child: child,
729
+ );
730
+ }
731
+
732
+ // ── Divider helper ────────────────────────────────────────────────────────
733
+
734
+ Widget _tabsDivider(BuildContext context) => Divider(
735
+ height: 1,
736
+ thickness: 1,
737
+ indent: KasySpacing.md,
738
+ endIndent: KasySpacing.md,
739
+ color: context.colors.outline.withValues(alpha: 0.33),
740
+ );
741
+
742
+ // ── Radio-style option row ────────────────────────────────────────────────
743
+
744
+ /// Single-select option row that looks like a radio button using KasyCheckbox.
745
+ class _RadioOptionTile extends StatelessWidget {
746
+ const _RadioOptionTile({
747
+ required this.label,
748
+ required this.description,
749
+ required this.selected,
750
+ required this.onTap,
751
+ });
752
+
753
+ final String label;
754
+ final String description;
755
+ final bool selected;
756
+ final VoidCallback onTap;
757
+
758
+ @override
759
+ Widget build(BuildContext context) {
760
+ return Padding(
761
+ padding: const EdgeInsets.symmetric(
762
+ horizontal: KasySpacing.md,
763
+ vertical: KasySpacing.smd,
764
+ ),
765
+ child: KasyCheckbox(
766
+ value: selected,
767
+ onChanged: (_) => onTap(),
768
+ label: label,
769
+ description: description,
770
+ style: const KasyCheckboxStyle(
771
+ shape: KasyCheckboxShape.circle,
772
+ ),
773
+ ),
774
+ );
775
+ }
776
+ }
777
+
670
778
  // ── Primary variant ────────────────────────────────────────────────────────
671
779
 
672
780
  class _TabsPrimaryPreview extends StatefulWidget {
@@ -679,40 +787,239 @@ class _TabsPrimaryPreview extends StatefulWidget {
679
787
  class _TabsPrimaryPreviewState extends State<_TabsPrimaryPreview> {
680
788
  int _index = 0;
681
789
 
682
- static const List<String> _tabs = ['Overview', 'Details', 'Settings'];
790
+ // General tab
791
+ final TextEditingController _urlController = TextEditingController();
792
+ bool _analytics = true;
793
+ bool _errorReports = false;
683
794
 
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
- ];
795
+ // Appearance tab
796
+ int _theme = 0; // 0=Auto 1=Light 2=Dark
797
+ int _fontSize = 1; // 0=Small 1=Medium 2=Large
798
+
799
+ // Notifications tab
800
+ bool _pushEnabled = true;
801
+ bool _emailEnabled = true;
802
+ bool _mentions = true;
803
+ bool _weeklyDigest = false;
804
+
805
+ // Profile tab
806
+ final TextEditingController _nameController =
807
+ TextEditingController(text: 'Paulo Morales');
808
+ final TextEditingController _usernameController =
809
+ TextEditingController(text: 'paulomorales');
689
810
 
690
811
  @override
691
- Widget build(BuildContext context) {
692
- final KasyColors c = context.colors;
812
+ void dispose() {
813
+ _urlController.dispose();
814
+ _nameController.dispose();
815
+ _usernameController.dispose();
816
+ super.dispose();
817
+ }
818
+
819
+ Widget _generalTab(BuildContext context) {
820
+ return Column(
821
+ crossAxisAlignment: CrossAxisAlignment.stretch,
822
+ mainAxisSize: MainAxisSize.min,
823
+ children: [
824
+ Padding(
825
+ padding: const EdgeInsets.all(KasySpacing.md),
826
+ child: KasyTextField(
827
+ controller: _urlController,
828
+ variant: KasyTextFieldVariant.secondary,
829
+ label: 'Homepage URL',
830
+ hint: 'https://yoursite.com',
831
+ ),
832
+ ),
833
+ _tabsDivider(context),
834
+ KasyCheckboxTile(
835
+ value: _analytics,
836
+ onChanged: (v) => setState(() => _analytics = v),
837
+ label: 'Enable analytics',
838
+ description: 'Collect anonymous usage data to improve the app',
839
+ ),
840
+ _tabsDivider(context),
841
+ KasyCheckboxTile(
842
+ value: _errorReports,
843
+ onChanged: (v) => setState(() => _errorReports = v),
844
+ label: 'Send error reports',
845
+ description: 'Automatically share crash reports with the team',
846
+ ),
847
+ ],
848
+ );
849
+ }
850
+
851
+ Widget _appearanceTab(BuildContext context) {
852
+ return Column(
853
+ crossAxisAlignment: CrossAxisAlignment.stretch,
854
+ mainAxisSize: MainAxisSize.min,
855
+ children: [
856
+ Padding(
857
+ padding: const EdgeInsets.fromLTRB(
858
+ KasySpacing.md,
859
+ KasySpacing.md,
860
+ KasySpacing.md,
861
+ KasySpacing.sm,
862
+ ),
863
+ child: Text(
864
+ 'Theme',
865
+ style: context.textTheme.labelMedium?.copyWith(
866
+ color: context.colors.onSurface,
867
+ fontWeight: FontWeight.w600,
868
+ ),
869
+ ),
870
+ ),
871
+ _RadioOptionTile(
872
+ label: 'Auto',
873
+ description: 'Follows your system appearance',
874
+ selected: _theme == 0,
875
+ onTap: () => setState(() => _theme = 0),
876
+ ),
877
+ _tabsDivider(context),
878
+ _RadioOptionTile(
879
+ label: 'Light',
880
+ description: 'Always use the light theme',
881
+ selected: _theme == 1,
882
+ onTap: () => setState(() => _theme = 1),
883
+ ),
884
+ _tabsDivider(context),
885
+ _RadioOptionTile(
886
+ label: 'Dark',
887
+ description: 'Always use the dark theme',
888
+ selected: _theme == 2,
889
+ onTap: () => setState(() => _theme = 2),
890
+ ),
891
+ _tabsDivider(context),
892
+ Padding(
893
+ padding: const EdgeInsets.fromLTRB(
894
+ KasySpacing.md,
895
+ KasySpacing.md,
896
+ KasySpacing.md,
897
+ KasySpacing.sm,
898
+ ),
899
+ child: Text(
900
+ 'Font size',
901
+ style: context.textTheme.labelMedium?.copyWith(
902
+ color: context.colors.onSurface,
903
+ fontWeight: FontWeight.w600,
904
+ ),
905
+ ),
906
+ ),
907
+ _RadioOptionTile(
908
+ label: 'Small',
909
+ description: 'Compact text for more content on screen',
910
+ selected: _fontSize == 0,
911
+ onTap: () => setState(() => _fontSize = 0),
912
+ ),
913
+ _tabsDivider(context),
914
+ _RadioOptionTile(
915
+ label: 'Medium',
916
+ description: 'Balanced size for most displays',
917
+ selected: _fontSize == 1,
918
+ onTap: () => setState(() => _fontSize = 1),
919
+ ),
920
+ _tabsDivider(context),
921
+ _RadioOptionTile(
922
+ label: 'Large',
923
+ description: 'Larger text for better readability',
924
+ selected: _fontSize == 2,
925
+ onTap: () => setState(() => _fontSize = 2),
926
+ ),
927
+ ],
928
+ );
929
+ }
930
+
931
+ Widget _notificationsTab(BuildContext context) {
932
+ return Column(
933
+ crossAxisAlignment: CrossAxisAlignment.stretch,
934
+ mainAxisSize: MainAxisSize.min,
935
+ children: [
936
+ KasyCheckboxTile(
937
+ value: _pushEnabled,
938
+ onChanged: (v) => setState(() => _pushEnabled = v),
939
+ label: 'Push notifications',
940
+ description: 'Receive alerts directly on your device',
941
+ ),
942
+ _tabsDivider(context),
943
+ KasyCheckboxTile(
944
+ value: _emailEnabled,
945
+ onChanged: (v) => setState(() => _emailEnabled = v),
946
+ label: 'Email notifications',
947
+ description: 'Get updates delivered to your inbox',
948
+ ),
949
+ _tabsDivider(context),
950
+ KasyCheckboxTile(
951
+ value: _mentions,
952
+ onChanged: (v) => setState(() => _mentions = v),
953
+ label: 'Mentions and replies',
954
+ description: 'Notify when someone mentions or replies to you',
955
+ ),
956
+ _tabsDivider(context),
957
+ KasyCheckboxTile(
958
+ value: _weeklyDigest,
959
+ onChanged: (v) => setState(() => _weeklyDigest = v),
960
+ label: 'Weekly digest',
961
+ description: 'A summary of activity sent every Monday',
962
+ ),
963
+ ],
964
+ );
965
+ }
966
+
967
+ Widget _profileTab(BuildContext context) {
968
+ return Padding(
969
+ padding: const EdgeInsets.all(KasySpacing.md),
970
+ child: Column(
971
+ crossAxisAlignment: CrossAxisAlignment.stretch,
972
+ mainAxisSize: MainAxisSize.min,
973
+ children: [
974
+ KasyTextField(
975
+ controller: _nameController,
976
+ variant: KasyTextFieldVariant.secondary,
977
+ label: 'Name',
978
+ hint: 'Your full name',
979
+ textInputAction: TextInputAction.next,
980
+ ),
981
+ const SizedBox(height: KasySpacing.md),
982
+ KasyTextField(
983
+ controller: _usernameController,
984
+ variant: KasyTextFieldVariant.secondary,
985
+ label: 'Username',
986
+ hint: 'yourhandle',
987
+ textInputAction: TextInputAction.done,
988
+ ),
989
+ const SizedBox(height: KasySpacing.md),
990
+ KasyButton(
991
+ label: 'Update profile',
992
+ onPressed: () {},
993
+ ),
994
+ ],
995
+ ),
996
+ );
997
+ }
693
998
 
999
+ @override
1000
+ Widget build(BuildContext context) {
694
1001
  return Column(
695
1002
  crossAxisAlignment: CrossAxisAlignment.stretch,
696
1003
  mainAxisSize: MainAxisSize.min,
697
1004
  children: [
698
1005
  KasyTabs(
699
- tabs: _tabs,
1006
+ tabs: const ['General', 'Appearance', 'Notifications', 'Profile'],
700
1007
  selectedIndex: _index,
701
1008
  onTabSelected: (i) => setState(() => _index = i),
702
1009
  ),
703
1010
  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
- ),
1011
+ // IndexedStack keeps height fixed at the tallest child (Appearance).
1012
+ // AnimatedOpacity on each child provides the smooth crossfade.
1013
+ _tabsCard(
1014
+ context,
1015
+ IndexedStack(
1016
+ index: _index,
1017
+ children: [
1018
+ _tabFade(0, _index, _generalTab(context)),
1019
+ _tabFade(1, _index, _appearanceTab(context)),
1020
+ _tabFade(2, _index, _notificationsTab(context)),
1021
+ _tabFade(3, _index, _profileTab(context)),
1022
+ ],
716
1023
  ),
717
1024
  ),
718
1025
  const SizedBox(height: KasySpacing.lg),
@@ -720,7 +1027,7 @@ class _TabsPrimaryPreviewState extends State<_TabsPrimaryPreview> {
720
1027
  Text(
721
1028
  'WITH DISABLED TAB',
722
1029
  style: context.textTheme.labelSmall?.copyWith(
723
- color: c.muted,
1030
+ color: context.colors.muted,
724
1031
  letterSpacing: 1.2,
725
1032
  fontWeight: FontWeight.w700,
726
1033
  ),
@@ -752,41 +1059,146 @@ class _TabsSecondaryPreview extends StatefulWidget {
752
1059
  class _TabsSecondaryPreviewState extends State<_TabsSecondaryPreview> {
753
1060
  int _index = 0;
754
1061
 
755
- static const List<String> _tabs = ['Inbox', 'Sent', 'Drafts'];
1062
+ // General tab
1063
+ final TextEditingController _siteController = TextEditingController();
1064
+ bool _cacheEnabled = true;
1065
+ bool _debugMode = false;
756
1066
 
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
- ];
1067
+ // Notifications tab
1068
+ bool _pushEnabled = true;
1069
+ bool _emailEnabled = false;
1070
+ bool _weeklyDigest = true;
1071
+
1072
+ // Profile tab
1073
+ final TextEditingController _nameController =
1074
+ TextEditingController(text: 'Paulo Morales');
1075
+ final TextEditingController _emailController =
1076
+ TextEditingController(text: 'paulo@kasy.dev');
762
1077
 
763
1078
  @override
764
- Widget build(BuildContext context) {
765
- final KasyColors c = context.colors;
1079
+ void dispose() {
1080
+ _siteController.dispose();
1081
+ _nameController.dispose();
1082
+ _emailController.dispose();
1083
+ super.dispose();
1084
+ }
1085
+
1086
+ Widget _generalTab(BuildContext context) {
1087
+ return Column(
1088
+ crossAxisAlignment: CrossAxisAlignment.stretch,
1089
+ mainAxisSize: MainAxisSize.min,
1090
+ children: [
1091
+ Padding(
1092
+ padding: const EdgeInsets.all(KasySpacing.md),
1093
+ child: KasyTextField(
1094
+ controller: _siteController,
1095
+ variant: KasyTextFieldVariant.secondary,
1096
+ label: 'Site URL',
1097
+ hint: 'https://yoursite.com',
1098
+ ),
1099
+ ),
1100
+ _tabsDivider(context),
1101
+ KasyCheckboxTile(
1102
+ value: _cacheEnabled,
1103
+ onChanged: (v) => setState(() => _cacheEnabled = v),
1104
+ label: 'Enable caching',
1105
+ description: 'Speed up page loads with local cache',
1106
+ ),
1107
+ _tabsDivider(context),
1108
+ KasyCheckboxTile(
1109
+ value: _debugMode,
1110
+ onChanged: (v) => setState(() => _debugMode = v),
1111
+ label: 'Debug mode',
1112
+ description: 'Show detailed logs and error messages',
1113
+ ),
1114
+ ],
1115
+ );
1116
+ }
766
1117
 
1118
+ Widget _notificationsTab(BuildContext context) {
1119
+ return Column(
1120
+ crossAxisAlignment: CrossAxisAlignment.stretch,
1121
+ mainAxisSize: MainAxisSize.min,
1122
+ children: [
1123
+ KasyCheckboxTile(
1124
+ value: _pushEnabled,
1125
+ onChanged: (v) => setState(() => _pushEnabled = v),
1126
+ label: 'Push notifications',
1127
+ description: 'Receive alerts directly on your device',
1128
+ ),
1129
+ _tabsDivider(context),
1130
+ KasyCheckboxTile(
1131
+ value: _emailEnabled,
1132
+ onChanged: (v) => setState(() => _emailEnabled = v),
1133
+ label: 'Email notifications',
1134
+ description: 'Get updates delivered to your inbox',
1135
+ ),
1136
+ _tabsDivider(context),
1137
+ KasyCheckboxTile(
1138
+ value: _weeklyDigest,
1139
+ onChanged: (v) => setState(() => _weeklyDigest = v),
1140
+ label: 'Weekly digest',
1141
+ description: 'A summary of activity sent every Monday',
1142
+ ),
1143
+ ],
1144
+ );
1145
+ }
1146
+
1147
+ Widget _profileTab(BuildContext context) {
1148
+ return Padding(
1149
+ padding: const EdgeInsets.all(KasySpacing.md),
1150
+ child: Column(
1151
+ crossAxisAlignment: CrossAxisAlignment.stretch,
1152
+ mainAxisSize: MainAxisSize.min,
1153
+ children: [
1154
+ KasyTextField(
1155
+ controller: _nameController,
1156
+ variant: KasyTextFieldVariant.secondary,
1157
+ label: 'Name',
1158
+ hint: 'Your full name',
1159
+ textInputAction: TextInputAction.next,
1160
+ ),
1161
+ const SizedBox(height: KasySpacing.md),
1162
+ KasyTextField(
1163
+ controller: _emailController,
1164
+ variant: KasyTextFieldVariant.secondary,
1165
+ label: 'Email',
1166
+ hint: 'email@example.com',
1167
+ contentType: KasyTextFieldContentType.email,
1168
+ textInputAction: TextInputAction.done,
1169
+ ),
1170
+ const SizedBox(height: KasySpacing.md),
1171
+ KasyButton(
1172
+ label: 'Save changes',
1173
+ onPressed: () {},
1174
+ ),
1175
+ ],
1176
+ ),
1177
+ );
1178
+ }
1179
+
1180
+ @override
1181
+ Widget build(BuildContext context) {
767
1182
  return Column(
768
1183
  crossAxisAlignment: CrossAxisAlignment.stretch,
769
1184
  mainAxisSize: MainAxisSize.min,
770
1185
  children: [
771
1186
  KasyTabs(
772
- tabs: _tabs,
1187
+ tabs: const ['General', 'Notifications', 'Profile'],
773
1188
  selectedIndex: _index,
774
1189
  onTabSelected: (i) => setState(() => _index = i),
775
1190
  variant: KasyTabsVariant.secondary,
776
1191
  ),
777
1192
  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
- ),
1193
+ _tabsCard(
1194
+ context,
1195
+ IndexedStack(
1196
+ index: _index,
1197
+ children: [
1198
+ _tabFade(0, _index, _generalTab(context)),
1199
+ _tabFade(1, _index, _notificationsTab(context)),
1200
+ _tabFade(2, _index, _profileTab(context)),
1201
+ ],
790
1202
  ),
791
1203
  ),
792
1204
  const SizedBox(height: KasySpacing.lg),
@@ -794,7 +1206,7 @@ class _TabsSecondaryPreviewState extends State<_TabsSecondaryPreview> {
794
1206
  Text(
795
1207
  'WITH ICONS',
796
1208
  style: context.textTheme.labelSmall?.copyWith(
797
- color: c.muted,
1209
+ color: context.colors.muted,
798
1210
  letterSpacing: 1.2,
799
1211
  fontWeight: FontWeight.w700,
800
1212
  ),
@@ -826,14 +1238,19 @@ class _TabsFillModePreview extends StatefulWidget {
826
1238
 
827
1239
  class _TabsFillModePreviewState extends State<_TabsFillModePreview> {
828
1240
  int _primaryIndex = 0;
829
- int _secondaryIndex = 0;
1241
+ int _iconIndex = 0;
1242
+
1243
+ static const List<String> _primaryContent = [
1244
+ 'Overview — see a high-level summary of your project status and recent activity.',
1245
+ 'Analytics — track metrics like sessions, retention, and conversion rates over time.',
1246
+ 'Settings — manage project configuration, integrations, and team access.',
1247
+ ];
830
1248
 
831
1249
  @override
832
1250
  Widget build(BuildContext context) {
833
- final KasyColors c = context.colors;
834
- final TextStyle labelStyle =
1251
+ final TextStyle sectionLabel =
835
1252
  context.textTheme.labelSmall?.copyWith(
836
- color: c.muted,
1253
+ color: context.colors.muted,
837
1254
  letterSpacing: 1.2,
838
1255
  fontWeight: FontWeight.w700,
839
1256
  ) ??
@@ -843,38 +1260,52 @@ class _TabsFillModePreviewState extends State<_TabsFillModePreview> {
843
1260
  crossAxisAlignment: CrossAxisAlignment.stretch,
844
1261
  mainAxisSize: MainAxisSize.min,
845
1262
  children: [
846
- Text('PRIMARY + FILL', style: labelStyle),
1263
+ Text('PRIMARY FILL', style: sectionLabel),
847
1264
  const SizedBox(height: KasySpacing.sm),
848
1265
  KasyTabs(
849
- tabs: const ['Tab 1', 'Tab 2', 'Tab 3'],
1266
+ tabs: const ['Overview', 'Analytics', 'Settings'],
850
1267
  selectedIndex: _primaryIndex,
851
1268
  onTabSelected: (i) => setState(() => _primaryIndex = i),
852
1269
  mode: KasyTabsMode.fill,
853
1270
  ),
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
1271
  const SizedBox(height: KasySpacing.md),
865
- AnimatedSwitcher(
866
- duration: const Duration(milliseconds: 200),
867
- child: KasyCard(
868
- key: ValueKey('fill_$_secondaryIndex'),
1272
+ _tabsCard(
1273
+ context,
1274
+ Padding(
869
1275
  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),
1276
+ child: IndexedStack(
1277
+ index: _primaryIndex,
1278
+ children: List.generate(
1279
+ _primaryContent.length,
1280
+ (i) => _tabFade(
1281
+ i,
1282
+ _primaryIndex,
1283
+ Text(
1284
+ _primaryContent[i],
1285
+ style: context.textTheme.bodyMedium?.copyWith(
1286
+ color: context.colors.onSurface.withValues(alpha: 0.75),
1287
+ height: 1.5,
1288
+ ),
1289
+ ),
1290
+ ),
874
1291
  ),
875
1292
  ),
876
1293
  ),
877
1294
  ),
1295
+ const SizedBox(height: KasySpacing.lg),
1296
+ Text('SECONDARY FILL + ICONS', style: sectionLabel),
1297
+ const SizedBox(height: KasySpacing.sm),
1298
+ KasyTabs.items(
1299
+ items: const [
1300
+ KasyTabItem(label: 'Home', icon: KasyIcons.home),
1301
+ KasyTabItem(label: 'Profile', icon: KasyIcons.person),
1302
+ KasyTabItem(label: 'Settings', icon: KasyIcons.settings),
1303
+ ],
1304
+ selectedIndex: _iconIndex,
1305
+ onTabSelected: (i) => setState(() => _iconIndex = i),
1306
+ variant: KasyTabsVariant.secondary,
1307
+ mode: KasyTabsMode.fill,
1308
+ ),
878
1309
  ],
879
1310
  );
880
1311
  }
@@ -1199,7 +1630,7 @@ Widget _buildAvatarGroupVariant(BuildContext context) {
1199
1630
  // Shared gradient avatars list
1200
1631
  final List<Widget> gradients = [
1201
1632
  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),
1633
+ KasyAvatar.gradientFill(size: KasyAvatarSize.small, diameter: d, showShadow: false, gradient: KasyAvatarGradients.sky),
1203
1634
  KasyAvatar.gradientFill(size: KasyAvatarSize.small, diameter: d, showShadow: false, gradient: KasyAvatarGradients.orange),
1204
1635
  KasyAvatar.gradientFill(size: KasyAvatarSize.small, diameter: d, showShadow: false, gradient: KasyAvatarGradients.red),
1205
1636
  KasyAvatar.gradientFill(size: KasyAvatarSize.small, diameter: d, showShadow: false, gradient: KasyAvatarGradients.silver),
@@ -2096,7 +2527,7 @@ Widget _buildBadgeDefault(BuildContext context) {
2096
2527
  child: KasyAvatar.gradientFill(
2097
2528
  size: KasyAvatarSize.medium,
2098
2529
  showShadow: false,
2099
- gradient: KasyAvatarGradients.teal,
2530
+ gradient: KasyAvatarGradients.sky,
2100
2531
  ),
2101
2532
  ),
2102
2533
  const SizedBox(height: KasySpacing.sm),
@@ -4028,9 +4459,9 @@ class _BadgeColorsPreview extends StatelessWidget {
4028
4459
  KasyBadgeTone.warning,
4029
4460
  KasyBadgeTone.danger,
4030
4461
  ];
4031
- final List<Gradient> gradients = [
4462
+ final List<KasyAvatarGradientData> gradients = [
4032
4463
  KasyAvatarGradients.blue,
4033
- KasyAvatarGradients.teal,
4464
+ KasyAvatarGradients.sky,
4034
4465
  KasyAvatarGradients.purple,
4035
4466
  KasyAvatarGradients.orange,
4036
4467
  KasyAvatarGradients.red,
@@ -4153,9 +4584,9 @@ class _BadgePlacementsPreview extends StatelessWidget {
4153
4584
  class _BadgeDotPreview extends StatelessWidget {
4154
4585
  const _BadgeDotPreview();
4155
4586
 
4156
- static const List<(KasyBadgeTone, Gradient)> _items = [
4587
+ static const List<(KasyBadgeTone, KasyAvatarGradientData)> _items = [
4157
4588
  (KasyBadgeTone.neutral, KasyAvatarGradients.blue),
4158
- (KasyBadgeTone.primary, KasyAvatarGradients.teal),
4589
+ (KasyBadgeTone.primary, KasyAvatarGradients.sky),
4159
4590
  (KasyBadgeTone.success, KasyAvatarGradients.purple),
4160
4591
  (KasyBadgeTone.warning, KasyAvatarGradients.red),
4161
4592
  (KasyBadgeTone.danger, KasyAvatarGradients.orange),
@@ -4197,9 +4628,9 @@ class _BadgeVariantsPreview extends StatelessWidget {
4197
4628
  KasyBadgeTone.danger,
4198
4629
  ];
4199
4630
 
4200
- static const List<Gradient> _gradients = [
4631
+ static const List<KasyAvatarGradientData> _gradients = [
4201
4632
  KasyAvatarGradients.blue,
4202
- KasyAvatarGradients.teal,
4633
+ KasyAvatarGradients.sky,
4203
4634
  KasyAvatarGradients.purple,
4204
4635
  KasyAvatarGradients.red,
4205
4636
  KasyAvatarGradients.orange,
@@ -4332,7 +4763,7 @@ class _BadgeWithContentPreview extends StatelessWidget {
4332
4763
  class _BadgeContentItem extends StatelessWidget {
4333
4764
  final String label;
4334
4765
  final KasyBadge badge;
4335
- final Gradient gradient;
4766
+ final KasyAvatarGradientData gradient;
4336
4767
 
4337
4768
  const _BadgeContentItem({
4338
4769
  required this.label,
@@ -8119,3 +8550,444 @@ class _SwipePreviewTile extends StatelessWidget {
8119
8550
  );
8120
8551
  }
8121
8552
  }
8553
+
8554
+ // ─────────────────────────────────────────────────────────────────────────────
8555
+ // DatePicker — Presentations
8556
+ // ─────────────────────────────────────────────────────────────────────────────
8557
+
8558
+ /// Maps the running app's locale onto a [KasyDatePickerLocale] preset so the
8559
+ /// preview demonstrates the same regional defaults (month names, week start,
8560
+ /// date format) the user would see in their own app.
8561
+ KasyDatePickerLocale _datePickerLocaleForApp(BuildContext context) {
8562
+ switch (Localizations.localeOf(context).languageCode) {
8563
+ case 'es':
8564
+ return KasyDatePickerLocale.es;
8565
+ case 'pt':
8566
+ return KasyDatePickerLocale.pt;
8567
+ default:
8568
+ return KasyDatePickerLocale.en;
8569
+ }
8570
+ }
8571
+
8572
+ Widget _buildDatePickerPresentations(BuildContext context) =>
8573
+ const _DatePickerPresentationsPreview();
8574
+
8575
+ class _DatePickerPresentationsPreview extends StatefulWidget {
8576
+ const _DatePickerPresentationsPreview();
8577
+
8578
+ @override
8579
+ State<_DatePickerPresentationsPreview> createState() =>
8580
+ _DatePickerPresentationsPreviewState();
8581
+ }
8582
+
8583
+ class _DatePickerPresentationsPreviewState
8584
+ extends State<_DatePickerPresentationsPreview> {
8585
+ DateTime? _popoverDate;
8586
+ DateTime? _dialogDate;
8587
+ DateTime? _sheetDate;
8588
+
8589
+ @override
8590
+ Widget build(BuildContext context) {
8591
+ final KasyDatePickerLocale locale = _datePickerLocaleForApp(context);
8592
+ return Column(
8593
+ mainAxisSize: MainAxisSize.min,
8594
+ crossAxisAlignment: CrossAxisAlignment.stretch,
8595
+ children: [
8596
+ KasyDatePicker(
8597
+ label: 'Popover',
8598
+ placeholder: 'Choose a date',
8599
+ value: _popoverDate,
8600
+ onChanged: (d) => setState(() => _popoverDate = d),
8601
+ locale: locale,
8602
+ ),
8603
+ const SizedBox(height: KasySpacing.lg),
8604
+ KasyDatePicker(
8605
+ label: 'Dialog',
8606
+ placeholder: 'Choose a date',
8607
+ value: _dialogDate,
8608
+ onChanged: (d) => setState(() => _dialogDate = d),
8609
+ presentation: KasyDatePickerPresentation.dialog,
8610
+ locale: locale,
8611
+ ),
8612
+ const SizedBox(height: KasySpacing.lg),
8613
+ KasyDatePicker(
8614
+ label: 'Bottom sheet',
8615
+ placeholder: 'Choose a date',
8616
+ value: _sheetDate,
8617
+ onChanged: (d) => setState(() => _sheetDate = d),
8618
+ presentation: KasyDatePickerPresentation.bottomSheet,
8619
+ locale: locale,
8620
+ ),
8621
+ ],
8622
+ );
8623
+ }
8624
+ }
8625
+
8626
+ // ─────────────────────────────────────────────────────────────────────────────
8627
+ // DatePicker — Range selection (Airbnb-style start + end)
8628
+ // ─────────────────────────────────────────────────────────────────────────────
8629
+
8630
+ Widget _buildDatePickerRange(BuildContext context) =>
8631
+ const _DatePickerRangePreview();
8632
+
8633
+ class _DatePickerRangePreview extends StatefulWidget {
8634
+ const _DatePickerRangePreview();
8635
+
8636
+ @override
8637
+ State<_DatePickerRangePreview> createState() =>
8638
+ _DatePickerRangePreviewState();
8639
+ }
8640
+
8641
+ class _DatePickerRangePreviewState extends State<_DatePickerRangePreview> {
8642
+ KasyDateRange? _stay;
8643
+ KasyDateRange? _project;
8644
+
8645
+ @override
8646
+ Widget build(BuildContext context) {
8647
+ final KasyDatePickerLocale locale = _datePickerLocaleForApp(context);
8648
+ return Column(
8649
+ mainAxisSize: MainAxisSize.min,
8650
+ crossAxisAlignment: CrossAxisAlignment.stretch,
8651
+ children: [
8652
+ // Classic stay-style picker: tap a check-in date, then a check-out
8653
+ // date. The calendar keeps the start highlighted while waiting for
8654
+ // the end, then auto-closes after the second tap.
8655
+ KasyDatePicker(
8656
+ label: 'Stay',
8657
+ placeholder: 'Check-in -> Check-out',
8658
+ description: 'Tap a start date, then an end date.',
8659
+ selectionMode: KasyDateSelectionMode.range,
8660
+ range: _stay,
8661
+ onRangeChanged: (r) => setState(() => _stay = r),
8662
+ locale: locale,
8663
+ ),
8664
+ const SizedBox(height: KasySpacing.lg),
8665
+ // Dialog presentation of the same range mode — handy on smaller
8666
+ // screens where the popover may not have enough room.
8667
+ KasyDatePicker(
8668
+ label: 'Project window (dialog)',
8669
+ placeholder: 'Pick start -> end',
8670
+ selectionMode: KasyDateSelectionMode.range,
8671
+ range: _project,
8672
+ onRangeChanged: (r) => setState(() => _project = r),
8673
+ presentation: KasyDatePickerPresentation.dialog,
8674
+ locale: locale,
8675
+ ),
8676
+ ],
8677
+ );
8678
+ }
8679
+ }
8680
+
8681
+ // ─────────────────────────────────────────────────────────────────────────────
8682
+ // DatePicker — Multi-month (2 or 3 months side by side, Airbnb desktop look)
8683
+ // ─────────────────────────────────────────────────────────────────────────────
8684
+
8685
+ Widget _buildDatePickerMultiMonth(BuildContext context) =>
8686
+ const _DatePickerMultiMonthPreview();
8687
+
8688
+ class _DatePickerMultiMonthPreview extends StatefulWidget {
8689
+ const _DatePickerMultiMonthPreview();
8690
+
8691
+ @override
8692
+ State<_DatePickerMultiMonthPreview> createState() =>
8693
+ _DatePickerMultiMonthPreviewState();
8694
+ }
8695
+
8696
+ class _DatePickerMultiMonthPreviewState
8697
+ extends State<_DatePickerMultiMonthPreview> {
8698
+ KasyDateRange? _stay;
8699
+ DateTime? _single;
8700
+
8701
+ @override
8702
+ Widget build(BuildContext context) {
8703
+ final KasyDatePickerLocale locale = _datePickerLocaleForApp(context);
8704
+ return Column(
8705
+ mainAxisSize: MainAxisSize.min,
8706
+ crossAxisAlignment: CrossAxisAlignment.stretch,
8707
+ children: [
8708
+ // 3-month layout combined with range selection — the classic
8709
+ // Airbnb / Booking desktop pattern for picking a stay.
8710
+ KasyDatePicker(
8711
+ label: '3 months + range',
8712
+ placeholder: 'Check-in -> Check-out',
8713
+ description: 'Pass monthsToShow: 3 to opt in. Best on wide canvases.',
8714
+ selectionMode: KasyDateSelectionMode.range,
8715
+ range: _stay,
8716
+ onRangeChanged: (r) => setState(() => _stay = r),
8717
+ monthsToShow: 3,
8718
+ locale: locale,
8719
+ ),
8720
+ const SizedBox(height: KasySpacing.lg),
8721
+ // 2-month layout in single-date mode — useful when there's enough
8722
+ // horizontal room but you still want one tap to commit.
8723
+ KasyDatePicker(
8724
+ label: '2 months + single date',
8725
+ placeholder: 'Pick a date',
8726
+ value: _single,
8727
+ onChanged: (d) => setState(() => _single = d),
8728
+ monthsToShow: 2,
8729
+ locale: locale,
8730
+ ),
8731
+ ],
8732
+ );
8733
+ }
8734
+ }
8735
+
8736
+ // ─────────────────────────────────────────────────────────────────────────────
8737
+ // DatePicker — Variants (filled, no backdrop, no suffix, no focus border)
8738
+ // ─────────────────────────────────────────────────────────────────────────────
8739
+
8740
+ Widget _buildDatePickerVariants(BuildContext context) =>
8741
+ const _DatePickerVariantsPreview();
8742
+
8743
+ class _DatePickerVariantsPreview extends StatefulWidget {
8744
+ const _DatePickerVariantsPreview();
8745
+
8746
+ @override
8747
+ State<_DatePickerVariantsPreview> createState() =>
8748
+ _DatePickerVariantsPreviewState();
8749
+ }
8750
+
8751
+ class _DatePickerVariantsPreviewState
8752
+ extends State<_DatePickerVariantsPreview> {
8753
+ DateTime? _filledDate;
8754
+ DateTime? _noBarrierDate;
8755
+ DateTime? _noSuffixDate;
8756
+ DateTime? _noFocusDate;
8757
+
8758
+ @override
8759
+ Widget build(BuildContext context) {
8760
+ final KasyDatePickerLocale locale = _datePickerLocaleForApp(context);
8761
+ return Column(
8762
+ mainAxisSize: MainAxisSize.min,
8763
+ crossAxisAlignment: CrossAxisAlignment.stretch,
8764
+ children: [
8765
+ // Filled / flat surface — matches KasyTextField.secondary, useful on
8766
+ // backgrounds where the default drop shadow feels heavy.
8767
+ KasyDatePicker(
8768
+ label: 'Filled (no shadow)',
8769
+ placeholder: 'Choose a date',
8770
+ value: _filledDate,
8771
+ onChanged: (d) => setState(() => _filledDate = d),
8772
+ variant: KasyTextFieldVariant.secondary,
8773
+ locale: locale,
8774
+ ),
8775
+ const SizedBox(height: KasySpacing.lg),
8776
+ // Popover without the dimmed backdrop — feels lighter / tooltip-like.
8777
+ KasyDatePicker(
8778
+ label: 'No backdrop',
8779
+ placeholder: 'Choose a date',
8780
+ description: 'Popover floats over the page without a dimmed scrim.',
8781
+ value: _noBarrierDate,
8782
+ onChanged: (d) => setState(() => _noBarrierDate = d),
8783
+ showBarrier: false,
8784
+ locale: locale,
8785
+ ),
8786
+ const SizedBox(height: KasySpacing.lg),
8787
+ // No calendar icon in the suffix slot.
8788
+ KasyDatePicker(
8789
+ label: 'No suffix icon',
8790
+ placeholder: 'Choose a date',
8791
+ value: _noSuffixDate,
8792
+ onChanged: (d) => setState(() => _noSuffixDate = d),
8793
+ showSuffix: false,
8794
+ locale: locale,
8795
+ ),
8796
+ const SizedBox(height: KasySpacing.lg),
8797
+ // Disables the blue focus outline while the calendar is open — quieter
8798
+ // affordance for very dense forms.
8799
+ KasyDatePicker(
8800
+ label: 'No focus border',
8801
+ placeholder: 'Choose a date',
8802
+ value: _noFocusDate,
8803
+ onChanged: (d) => setState(() => _noFocusDate = d),
8804
+ focusBorder: false,
8805
+ locale: locale,
8806
+ ),
8807
+ ],
8808
+ );
8809
+ }
8810
+ }
8811
+
8812
+ // ─────────────────────────────────────────────────────────────────────────────
8813
+ // DatePicker — Date formats
8814
+ // ─────────────────────────────────────────────────────────────────────────────
8815
+
8816
+ Widget _buildDatePickerFormats(BuildContext context) =>
8817
+ const _DatePickerFormatsPreview();
8818
+
8819
+ const List<String> _kEnglishMonthsLong = [
8820
+ 'January', 'February', 'March', 'April', 'May', 'June',
8821
+ 'July', 'August', 'September', 'October', 'November', 'December',
8822
+ ];
8823
+
8824
+ String _longUsFormat(DateTime d) =>
8825
+ '${_kEnglishMonthsLong[d.month - 1]} ${d.day}, ${d.year}';
8826
+
8827
+ class _DatePickerFormatsPreview extends StatefulWidget {
8828
+ const _DatePickerFormatsPreview();
8829
+
8830
+ @override
8831
+ State<_DatePickerFormatsPreview> createState() =>
8832
+ _DatePickerFormatsPreviewState();
8833
+ }
8834
+
8835
+ // Each tab maps to one preset (or to the "Auto" / "Custom" entries). The
8836
+ // `format` field is null for the two non-preset entries; the build method
8837
+ // branches on those.
8838
+ class _DatePickerFormatOption {
8839
+ const _DatePickerFormatOption({
8840
+ required this.label,
8841
+ required this.description,
8842
+ this.format,
8843
+ this.useAppLocale = false,
8844
+ this.customFormat,
8845
+ });
8846
+
8847
+ final String label;
8848
+ final String description;
8849
+ final KasyDateFormat? format;
8850
+ final bool useAppLocale;
8851
+ final String Function(DateTime)? customFormat;
8852
+ }
8853
+
8854
+ const List<_DatePickerFormatOption> _kDatePickerFormatOptions = [
8855
+ _DatePickerFormatOption(
8856
+ label: 'Auto',
8857
+ description: 'Follows the active app language (US, South American, …).',
8858
+ useAppLocale: true,
8859
+ ),
8860
+ _DatePickerFormatOption(
8861
+ label: 'US',
8862
+ description: 'MM / DD / YYYY — United States.',
8863
+ format: KasyDateFormat.us,
8864
+ ),
8865
+ _DatePickerFormatOption(
8866
+ label: 'South American',
8867
+ description: 'DD / MM / YYYY — South America, UK, Spain, Portugal.',
8868
+ format: KasyDateFormat.southAmerican,
8869
+ ),
8870
+ _DatePickerFormatOption(
8871
+ label: 'European',
8872
+ description: 'DD.MM.YYYY — Germany, Austria, Switzerland.',
8873
+ format: KasyDateFormat.european,
8874
+ ),
8875
+ _DatePickerFormatOption(
8876
+ label: 'ISO 8601',
8877
+ description: 'YYYY-MM-DD — international standard.',
8878
+ format: KasyDateFormat.iso,
8879
+ ),
8880
+ _DatePickerFormatOption(
8881
+ label: 'East Asian',
8882
+ description: 'YYYY / MM / DD — Japan, China, Korea.',
8883
+ format: KasyDateFormat.eastAsian,
8884
+ ),
8885
+ _DatePickerFormatOption(
8886
+ label: 'Custom',
8887
+ description: 'Any layout via the formatDate callback (long form here).',
8888
+ customFormat: _longUsFormat,
8889
+ ),
8890
+ ];
8891
+
8892
+ class _DatePickerFormatsPreviewState extends State<_DatePickerFormatsPreview> {
8893
+ int _tab = 0;
8894
+ DateTime _date = DateTime(2026, 5, 12);
8895
+
8896
+ @override
8897
+ Widget build(BuildContext context) {
8898
+ final _DatePickerFormatOption option = _kDatePickerFormatOptions[_tab];
8899
+ final KasyDatePickerLocale appLocale = _datePickerLocaleForApp(context);
8900
+
8901
+ return Column(
8902
+ mainAxisSize: MainAxisSize.min,
8903
+ crossAxisAlignment: CrossAxisAlignment.stretch,
8904
+ children: [
8905
+ // Secondary (underline) variant keeps the format switcher visually
8906
+ // light on phones — pill tabs were too bulky for a switch with many
8907
+ // labels and hug mode lets the row scroll horizontally when needed.
8908
+ KasyTabs(
8909
+ tabs: [
8910
+ for (final o in _kDatePickerFormatOptions) o.label,
8911
+ ],
8912
+ selectedIndex: _tab,
8913
+ onTabSelected: (i) => setState(() => _tab = i),
8914
+ variant: KasyTabsVariant.secondary,
8915
+ ),
8916
+ const SizedBox(height: KasySpacing.lg),
8917
+ KasyDatePicker(
8918
+ label: option.label,
8919
+ description: option.description,
8920
+ value: _date,
8921
+ onChanged: (d) => setState(() => _date = d),
8922
+ locale: option.useAppLocale ? appLocale : KasyDatePickerLocale.en,
8923
+ dateFormat: option.format,
8924
+ formatDate: option.customFormat,
8925
+ ),
8926
+ ],
8927
+ );
8928
+ }
8929
+ }
8930
+
8931
+ // ─────────────────────────────────────────────────────────────────────────────
8932
+ // DatePicker — Field states
8933
+ // ─────────────────────────────────────────────────────────────────────────────
8934
+
8935
+ Widget _buildDatePickerFieldStates(BuildContext context) =>
8936
+ const _DatePickerFieldStatesPreview();
8937
+
8938
+ const List<String> _kEnglishMonthAbbrev = [
8939
+ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
8940
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
8941
+ ];
8942
+
8943
+ String _abbrevFormat(DateTime d) =>
8944
+ '${_kEnglishMonthAbbrev[d.month - 1]} ${d.day}, ${d.year}';
8945
+
8946
+ class _DatePickerFieldStatesPreview extends StatefulWidget {
8947
+ const _DatePickerFieldStatesPreview();
8948
+
8949
+ @override
8950
+ State<_DatePickerFieldStatesPreview> createState() =>
8951
+ _DatePickerFieldStatesPreviewState();
8952
+ }
8953
+
8954
+ class _DatePickerFieldStatesPreviewState
8955
+ extends State<_DatePickerFieldStatesPreview> {
8956
+ DateTime? _deadline;
8957
+ DateTime? _shipDate;
8958
+ final DateTime _disabledDate = DateTime(2026, 6, 15);
8959
+
8960
+ @override
8961
+ Widget build(BuildContext context) {
8962
+ return Column(
8963
+ mainAxisSize: MainAxisSize.min,
8964
+ crossAxisAlignment: CrossAxisAlignment.stretch,
8965
+ children: [
8966
+ KasyDatePicker(
8967
+ label: 'Deadline',
8968
+ placeholder: 'Select a deadline',
8969
+ showRequiredIndicator: true,
8970
+ description: 'Required for the project timeline.',
8971
+ value: _deadline,
8972
+ onChanged: (d) => setState(() => _deadline = d),
8973
+ ),
8974
+ const SizedBox(height: KasySpacing.lg),
8975
+ KasyDatePicker(
8976
+ label: 'Ship date',
8977
+ placeholder: 'Choose a date',
8978
+ errorText: 'Please select a valid ship date.',
8979
+ value: _shipDate,
8980
+ onChanged: (d) => setState(() => _shipDate = d),
8981
+ ),
8982
+ const SizedBox(height: KasySpacing.lg),
8983
+ KasyDatePicker(
8984
+ label: 'Disabled date',
8985
+ enabled: false,
8986
+ value: _disabledDate,
8987
+ formatDate: _abbrevFormat,
8988
+ description: 'The date cannot be changed when the order is locked.',
8989
+ ),
8990
+ ],
8991
+ );
8992
+ }
8993
+ }