kasy-cli 1.37.1 → 1.39.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 (120) hide show
  1. package/lib/scaffold/CHANGELOG.json +23 -0
  2. package/lib/scaffold/backends/api/patch/README.md +15 -0
  3. package/lib/scaffold/backends/api/patch/lib/core/data/api/user_api.dart +18 -0
  4. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +11 -6
  5. package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +9 -1
  6. package/lib/scaffold/backends/api/pubspec.yaml.tpl +1 -0
  7. package/lib/scaffold/backends/patch-base-hashes.json +6 -6
  8. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +3 -1
  9. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +3 -0
  10. package/lib/scaffold/backends/supabase/migrations/20240101000012_welcome_decouple_from_push.sql +62 -0
  11. package/lib/scaffold/backends/supabase/patch/lib/core/data/api/user_api.dart +8 -0
  12. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +11 -6
  13. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  14. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +1 -0
  15. package/lib/scaffold/shared/generator-utils.js +12 -6
  16. package/package.json +1 -1
  17. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
  18. package/templates/firebase/AGENTS.md +7 -1
  19. package/templates/firebase/DESIGN_SYSTEM.md +35 -8
  20. package/templates/firebase/assets/icons/apple_black.svg +3 -0
  21. package/templates/firebase/assets/icons/apple_white.svg +4 -0
  22. package/templates/firebase/assets/icons/facebook.svg +49 -0
  23. package/templates/firebase/assets/icons/google.svg +1 -0
  24. package/templates/firebase/functions/src/admin/functions.ts +2 -0
  25. package/templates/firebase/functions/src/authentication/functions.ts +13 -7
  26. package/templates/firebase/functions/src/notifications/triggers.ts +6 -2
  27. package/templates/firebase/lib/components/components.dart +1 -1
  28. package/templates/firebase/lib/components/kasy_app_bar.dart +361 -20
  29. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +283 -66
  30. package/templates/firebase/lib/components/kasy_card.dart +4 -0
  31. package/templates/firebase/lib/components/kasy_date_picker.dart +61 -46
  32. package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
  33. package/templates/firebase/lib/components/kasy_sidebar.dart +412 -31
  34. package/templates/firebase/lib/components/kasy_tabs.dart +31 -10
  35. package/templates/firebase/lib/components/kasy_text_field.dart +29 -7
  36. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +29 -231
  37. package/templates/firebase/lib/core/bottom_menu/sidebar_focus.dart +224 -0
  38. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +19 -9
  39. package/templates/firebase/lib/core/chrome/app_bar_config.dart +214 -0
  40. package/templates/firebase/lib/core/chrome/app_bar_scope.dart +102 -0
  41. package/templates/firebase/lib/core/data/api/user_api.dart +15 -0
  42. package/templates/firebase/lib/core/data/repositories/user_repository.dart +5 -0
  43. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +525 -65
  44. package/templates/firebase/lib/core/dev_inspector/dev_inspector_info.dart +47 -0
  45. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
  46. package/templates/firebase/lib/core/icons/kasy_icons.dart +16 -1
  47. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +18 -35
  48. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +11 -0
  49. package/templates/firebase/lib/core/states/logout_action.dart +11 -1
  50. package/templates/firebase/lib/core/states/user_state_notifier.dart +69 -1
  51. package/templates/firebase/lib/core/theme/texts.dart +21 -6
  52. package/templates/firebase/lib/core/theme/type_scale.dart +34 -15
  53. package/templates/firebase/lib/core/theme/universal_theme.dart +9 -0
  54. package/templates/firebase/lib/core/web_device_preview/png_clipboard.dart +14 -0
  55. package/templates/firebase/lib/core/web_device_preview/png_clipboard_io.dart +9 -0
  56. package/templates/firebase/lib/core/web_device_preview/png_clipboard_web.dart +36 -0
  57. package/templates/firebase/lib/core/web_device_preview/png_export_result.dart +2 -0
  58. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +547 -483
  59. package/templates/firebase/lib/core/web_viewport_scale.dart +64 -35
  60. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +14 -3
  61. package/templates/firebase/lib/core/widgets/responsive_layout.dart +8 -0
  62. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +1 -1
  63. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +52 -35
  64. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
  65. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +18 -8
  66. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -61
  67. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +11 -61
  68. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +7 -5
  69. package/templates/firebase/lib/features/authentication/ui/widgets/social_auth_tile.dart +83 -0
  70. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +4 -4
  71. package/templates/firebase/lib/features/home/home_components_page.dart +264 -126
  72. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +231 -57
  73. package/templates/firebase/lib/features/home/home_feed.dart +2 -2
  74. package/templates/firebase/lib/features/home/home_image_grid.dart +3 -3
  75. package/templates/firebase/lib/features/local_reminders/providers/reminder_notifier.dart +8 -1
  76. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +118 -57
  77. package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
  78. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +19 -4
  79. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +7 -56
  80. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +5 -5
  81. package/templates/firebase/lib/features/notifications/ui/widgets/push_permission_banner.dart +163 -0
  82. package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +262 -0
  83. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +9 -0
  84. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +28 -0
  85. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +51 -12
  86. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +90 -32
  87. package/templates/firebase/lib/features/settings/settings_page.dart +99 -65
  88. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1379 -422
  89. package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +14 -4
  90. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  91. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +445 -233
  92. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +404 -149
  93. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +24 -31
  94. package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +9 -1
  95. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +77 -95
  96. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +21 -18
  97. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +25 -10
  98. package/templates/firebase/lib/i18n/en.i18n.json +749 -698
  99. package/templates/firebase/lib/i18n/es.i18n.json +749 -698
  100. package/templates/firebase/lib/i18n/pt.i18n.json +749 -698
  101. package/templates/firebase/lib/main.dart +20 -7
  102. package/templates/firebase/lib/router.dart +70 -46
  103. package/templates/firebase/pubspec.yaml +2 -1
  104. package/templates/firebase/test/admin_shell_chrome_test.dart +110 -0
  105. package/templates/firebase/test/components/kasy_text_field_height_test.dart +77 -0
  106. package/templates/firebase/test/core/web_viewport_scale_test.dart +23 -16
  107. package/templates/firebase/test/features/authentication/data/api/user_api_fake.dart +3 -0
  108. package/templates/firebase/tool/design_check.dart +9 -0
  109. package/templates/firebase/assets/icons/apple.png +0 -0
  110. package/templates/firebase/assets/icons/facebook.png +0 -0
  111. package/templates/firebase/assets/icons/google.png +0 -0
  112. package/templates/firebase/assets/icons/google_play_games.png +0 -0
  113. package/templates/firebase/lib/components/kasy_web_header.dart +0 -210
  114. package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +0 -19
  115. package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +0 -32
  116. package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +0 -73
  117. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -66
  118. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +0 -169
  119. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +0 -106
  120. package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +0 -53
@@ -71,39 +71,35 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
71
71
  return const ComponentPreviewDefinition(
72
72
  title: 'AppBar',
73
73
  variants: [
74
+ // Phone / tablet — the page chrome (title + back + orbs).
74
75
  ComponentPreviewVariant(
75
- label: 'Subpage',
76
+ label: 'Subpage (mobile)',
76
77
  builder: _buildAppBarSubpageVariant,
77
78
  ),
78
79
  ComponentPreviewVariant(
79
- label: 'Subpage Simple',
80
+ label: 'Subpage Simple (mobile)',
80
81
  builder: _buildAppBarSubpageSimpleVariant,
81
82
  ),
82
83
  ComponentPreviewVariant(
83
- label: 'Subpage Actions',
84
+ label: 'Subpage Actions (mobile)',
84
85
  builder: _buildAppBarSubpageActionsVariant,
85
86
  ),
86
87
  ComponentPreviewVariant(
87
- label: 'Menu opens sidebar',
88
+ label: 'Menu opens sidebar (mobile)',
88
89
  builder: _buildAppBarMenuVariant,
89
90
  ),
90
- ],
91
- );
92
- case 'Web Header':
93
- return const ComponentPreviewDefinition(
94
- title: 'Web Header',
95
- variants: [
91
+ // Desktop — the application chrome (search + actions + profile).
96
92
  ComponentPreviewVariant(
97
- label: 'Default',
98
- builder: _buildWebHeaderDefaultVariant,
93
+ label: 'Application (desktop)',
94
+ builder: _buildAppBarApplicationVariant,
99
95
  ),
100
96
  ComponentPreviewVariant(
101
- label: 'With notification',
102
- builder: _buildWebHeaderBadgeVariant,
97
+ label: 'Application + notification (desktop)',
98
+ builder: _buildAppBarApplicationBadgeVariant,
103
99
  ),
104
100
  ComponentPreviewVariant(
105
- label: 'Without avatar',
106
- builder: _buildWebHeaderNoAvatarVariant,
101
+ label: 'Application no avatar (desktop)',
102
+ builder: _buildAppBarApplicationNoAvatarVariant,
107
103
  ),
108
104
  ],
109
105
  );
@@ -309,6 +305,24 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
309
305
  ),
310
306
  ],
311
307
  );
308
+ case 'DropDown':
309
+ return const ComponentPreviewDefinition(
310
+ title: 'DropDown',
311
+ variants: [
312
+ ComponentPreviewVariant(
313
+ label: 'Basic',
314
+ builder: _buildDropDownBasic,
315
+ ),
316
+ ComponentPreviewVariant(
317
+ label: 'With icons & subtitles',
318
+ builder: _buildDropDownRich,
319
+ ),
320
+ ComponentPreviewVariant(
321
+ label: 'States',
322
+ builder: _buildDropDownStates,
323
+ ),
324
+ ],
325
+ );
312
326
  case 'Badge':
313
327
  return const ComponentPreviewDefinition(
314
328
  title: 'Badge',
@@ -1928,16 +1942,16 @@ Widget _buildAppBarMenuVariant(BuildContext context) {
1928
1942
  );
1929
1943
  }
1930
1944
 
1931
- Widget _buildWebHeaderDefaultVariant(BuildContext context) {
1932
- return const _WebHeaderPreview();
1945
+ Widget _buildAppBarApplicationVariant(BuildContext context) {
1946
+ return const _ApplicationBarPreview();
1933
1947
  }
1934
1948
 
1935
- Widget _buildWebHeaderBadgeVariant(BuildContext context) {
1936
- return const _WebHeaderPreview(showBadge: true);
1949
+ Widget _buildAppBarApplicationBadgeVariant(BuildContext context) {
1950
+ return const _ApplicationBarPreview(showBadge: true);
1937
1951
  }
1938
1952
 
1939
- Widget _buildWebHeaderNoAvatarVariant(BuildContext context) {
1940
- return const _WebHeaderPreview(showAvatar: false);
1953
+ Widget _buildAppBarApplicationNoAvatarVariant(BuildContext context) {
1954
+ return const _ApplicationBarPreview(showAvatar: false);
1941
1955
  }
1942
1956
 
1943
1957
  Widget _buildButtonSizesVariant(BuildContext context) {
@@ -3866,49 +3880,62 @@ class _AccordionPreviewState extends State<_AccordionPreview> {
3866
3880
  }
3867
3881
  }
3868
3882
 
3869
- /// Presents [KasyWebHeader] inside a desktop browser-window mock (title bar +
3870
- /// faux sidebar + content) so the preview reads as the web/desktop chrome it is.
3871
- class _WebHeaderPreview extends StatelessWidget {
3883
+ /// Presents [KasyAppBar.application] inside a desktop browser-window mock (title
3884
+ /// bar + faux sidebar + content) so the preview reads as the desktop chrome it
3885
+ /// is the responsive desktop half of [KasyAppBar].
3886
+ class _ApplicationBarPreview extends StatelessWidget {
3872
3887
  final bool showBadge;
3873
3888
  final bool showAvatar;
3874
3889
 
3875
- const _WebHeaderPreview({this.showBadge = false, this.showAvatar = true});
3890
+ const _ApplicationBarPreview({this.showBadge = false, this.showAvatar = true});
3891
+
3892
+ /// Width the mock window is laid out at. The application bar is desktop chrome
3893
+ /// (220px search + actions), so it needs a desktop-class width — we render at
3894
+ /// this width and scale the whole window down to fit the (narrow) preview card.
3895
+ /// Without this, the bar overflows on a phone-sized preview.
3896
+ static const double _mockWindowWidth = 760;
3876
3897
 
3877
3898
  @override
3878
3899
  Widget build(BuildContext context) {
3879
3900
  final KasyColors c = context.colors;
3880
- return DecoratedBox(
3881
- decoration: BoxDecoration(
3882
- color: c.surface,
3883
- borderRadius: BorderRadius.circular(KasyRadius.lg),
3884
- border: Border.all(color: c.onSurface.withValues(alpha: 0.10)),
3885
- boxShadow: [KasyShadows.component(context)],
3886
- ),
3887
- child: ClipRRect(
3888
- borderRadius: BorderRadius.circular(KasyRadius.lg),
3889
- child: Column(
3890
- mainAxisSize: MainAxisSize.min,
3891
- children: [
3892
- const _BrowserTopBar(),
3893
- // The real header, flush — no corner radius of its own.
3894
- KasyWebHeader(
3895
- showNotificationBadge: showBadge,
3896
- showAvatar: showAvatar,
3897
- onNotifications: () {},
3898
- onCreate: () {},
3899
- onAvatarTap: () {},
3900
- ),
3901
- const SizedBox(
3902
- height: 150,
3903
- child: Row(
3904
- crossAxisAlignment: CrossAxisAlignment.stretch,
3905
- children: [
3906
- _DesktopMockSidebar(),
3907
- Expanded(child: _DesktopMockContent()),
3908
- ],
3909
- ),
3901
+ return FittedBox(
3902
+ fit: BoxFit.scaleDown,
3903
+ child: SizedBox(
3904
+ width: _mockWindowWidth,
3905
+ child: DecoratedBox(
3906
+ decoration: BoxDecoration(
3907
+ color: c.surface,
3908
+ borderRadius: BorderRadius.circular(KasyRadius.lg),
3909
+ border: Border.all(color: c.onSurface.withValues(alpha: 0.10)),
3910
+ boxShadow: [KasyShadows.component(context)],
3911
+ ),
3912
+ child: ClipRRect(
3913
+ borderRadius: BorderRadius.circular(KasyRadius.lg),
3914
+ child: Column(
3915
+ mainAxisSize: MainAxisSize.min,
3916
+ children: [
3917
+ const _BrowserTopBar(),
3918
+ // The real bar, flush — no corner radius of its own.
3919
+ KasyAppBar.application(
3920
+ showNotificationBadge: showBadge,
3921
+ showAvatar: showAvatar,
3922
+ onNotifications: () {},
3923
+ onCreate: () {},
3924
+ onAvatarTap: () {},
3925
+ ),
3926
+ const SizedBox(
3927
+ height: 150,
3928
+ child: Row(
3929
+ crossAxisAlignment: CrossAxisAlignment.stretch,
3930
+ children: [
3931
+ _DesktopMockSidebar(),
3932
+ Expanded(child: _DesktopMockContent()),
3933
+ ],
3934
+ ),
3935
+ ),
3936
+ ],
3910
3937
  ),
3911
- ],
3938
+ ),
3912
3939
  ),
3913
3940
  ),
3914
3941
  );
@@ -9447,3 +9474,150 @@ class _DatePickerFieldStatesPreviewState
9447
9474
  );
9448
9475
  }
9449
9476
  }
9477
+
9478
+ // ─────────────────────────────────────────────────────────────────────────────
9479
+ // DropDown — single-select dropdown (HeroUI "Select")
9480
+ // ─────────────────────────────────────────────────────────────────────────────
9481
+
9482
+ const List<KasyDropDownItem<String>> _kDropDownStates = [
9483
+ KasyDropDownItem(value: 'fl', label: 'Florida'),
9484
+ KasyDropDownItem(value: 'de', label: 'Delaware'),
9485
+ KasyDropDownItem(value: 'tx', label: 'Texas'),
9486
+ KasyDropDownItem(value: 'ca', label: 'California'),
9487
+ KasyDropDownItem(value: 'ny', label: 'New York'),
9488
+ KasyDropDownItem(value: 'wy', label: 'Wyoming'),
9489
+ ];
9490
+
9491
+ Widget _buildDropDownBasic(BuildContext context) => const _DropDownBasicPreview();
9492
+
9493
+ class _DropDownBasicPreview extends StatefulWidget {
9494
+ const _DropDownBasicPreview();
9495
+
9496
+ @override
9497
+ State<_DropDownBasicPreview> createState() => _DropDownBasicPreviewState();
9498
+ }
9499
+
9500
+ class _DropDownBasicPreviewState extends State<_DropDownBasicPreview> {
9501
+ String? _state;
9502
+
9503
+ @override
9504
+ Widget build(BuildContext context) {
9505
+ return Column(
9506
+ mainAxisSize: MainAxisSize.min,
9507
+ crossAxisAlignment: CrossAxisAlignment.stretch,
9508
+ children: [
9509
+ KasyDropDown<String>(
9510
+ label: 'State',
9511
+ hint: 'Select one',
9512
+ showRequiredIndicator: true,
9513
+ value: _state,
9514
+ items: _kDropDownStates,
9515
+ onChanged: (v) => setState(() => _state = v),
9516
+ ),
9517
+ ],
9518
+ );
9519
+ }
9520
+ }
9521
+
9522
+ Widget _buildDropDownRich(BuildContext context) => const _DropDownRichPreview();
9523
+
9524
+ class _DropDownRichPreview extends StatefulWidget {
9525
+ const _DropDownRichPreview();
9526
+
9527
+ @override
9528
+ State<_DropDownRichPreview> createState() => _DropDownRichPreviewState();
9529
+ }
9530
+
9531
+ class _DropDownRichPreviewState extends State<_DropDownRichPreview> {
9532
+ String? _action;
9533
+
9534
+ @override
9535
+ Widget build(BuildContext context) {
9536
+ return Column(
9537
+ mainAxisSize: MainAxisSize.min,
9538
+ crossAxisAlignment: CrossAxisAlignment.stretch,
9539
+ children: [
9540
+ KasyDropDown<String>(
9541
+ label: 'Quick action',
9542
+ hint: 'Pick an action',
9543
+ leadingIcon: KasyIcons.idea,
9544
+ value: _action,
9545
+ items: const [
9546
+ KasyDropDownItem(
9547
+ value: 'new',
9548
+ label: 'New file',
9549
+ icon: KasyIcons.add,
9550
+ subtitle: 'Create a new file',
9551
+ ),
9552
+ KasyDropDownItem(
9553
+ value: 'copy',
9554
+ label: 'Copy link',
9555
+ icon: KasyIcons.copy,
9556
+ subtitle: 'Copy a shareable link',
9557
+ ),
9558
+ KasyDropDownItem(
9559
+ value: 'settings',
9560
+ label: 'Settings',
9561
+ icon: KasyIcons.settings,
9562
+ subtitle: 'Manage file permissions',
9563
+ ),
9564
+ ],
9565
+ onChanged: (v) => setState(() => _action = v),
9566
+ ),
9567
+ ],
9568
+ );
9569
+ }
9570
+ }
9571
+
9572
+ Widget _buildDropDownStates(BuildContext context) =>
9573
+ const _DropDownStatesPreview();
9574
+
9575
+ class _DropDownStatesPreview extends StatefulWidget {
9576
+ const _DropDownStatesPreview();
9577
+
9578
+ @override
9579
+ State<_DropDownStatesPreview> createState() => _DropDownStatesPreviewState();
9580
+ }
9581
+
9582
+ class _DropDownStatesPreviewState extends State<_DropDownStatesPreview> {
9583
+ String? _invalid;
9584
+
9585
+ @override
9586
+ Widget build(BuildContext context) {
9587
+ return Column(
9588
+ mainAxisSize: MainAxisSize.min,
9589
+ crossAxisAlignment: CrossAxisAlignment.stretch,
9590
+ children: [
9591
+ // Pre-selected value.
9592
+ KasyDropDown<String>(
9593
+ label: 'Selected',
9594
+ hint: 'Select one',
9595
+ value: 'ca',
9596
+ items: _kDropDownStates,
9597
+ onChanged: (_) {},
9598
+ ),
9599
+ const SizedBox(height: KasySpacing.lg),
9600
+ // Invalid + error text.
9601
+ KasyDropDown<String>(
9602
+ label: 'Required',
9603
+ hint: 'Select one',
9604
+ showRequiredIndicator: true,
9605
+ isInvalid: _invalid == null,
9606
+ errorText: _invalid == null ? 'Please choose a state.' : null,
9607
+ value: _invalid,
9608
+ items: _kDropDownStates,
9609
+ onChanged: (v) => setState(() => _invalid = v),
9610
+ ),
9611
+ const SizedBox(height: KasySpacing.lg),
9612
+ // Disabled trigger.
9613
+ const KasyDropDown<String>(
9614
+ label: 'Disabled',
9615
+ hint: 'Locked',
9616
+ enabled: false,
9617
+ items: _kDropDownStates,
9618
+ onChanged: null,
9619
+ ),
9620
+ ],
9621
+ );
9622
+ }
9623
+ }
@@ -336,7 +336,7 @@ class _FilterCard extends StatelessWidget {
336
336
  data.title,
337
337
  maxLines: 1,
338
338
  overflow: TextOverflow.ellipsis,
339
- style: context.textTheme.titleSmall?.copyWith(
339
+ style: context.kasyTextTheme.cardTitle.copyWith(
340
340
  color: selected ? c.primary : c.onSurface,
341
341
  ),
342
342
  ),
@@ -345,7 +345,7 @@ class _FilterCard extends StatelessWidget {
345
345
  data.subtitle,
346
346
  maxLines: 1,
347
347
  overflow: TextOverflow.ellipsis,
348
- style: context.textTheme.bodySmall?.copyWith(
348
+ style: context.kasyTextTheme.cardSubtitle.copyWith(
349
349
  color: c.muted,
350
350
  ),
351
351
  ),
@@ -234,7 +234,7 @@ class _PhotoTileState extends State<_PhotoTile> {
234
234
  photo.author,
235
235
  maxLines: 1,
236
236
  overflow: TextOverflow.ellipsis,
237
- style: context.textTheme.titleSmall?.copyWith(
237
+ style: context.kasyTextTheme.cardTitle.copyWith(
238
238
  color: context.colors.onSurface,
239
239
  ),
240
240
  ),
@@ -242,7 +242,7 @@ class _PhotoTileState extends State<_PhotoTile> {
242
242
  photo.ago,
243
243
  maxLines: 1,
244
244
  overflow: TextOverflow.ellipsis,
245
- style: context.textTheme.bodySmall?.copyWith(
245
+ style: context.kasyTextTheme.caption.copyWith(
246
246
  color: context.colors.muted,
247
247
  ),
248
248
  ),
@@ -375,7 +375,7 @@ class _LikeButtonState extends State<_LikeButton>
375
375
 
376
376
  return KasyFocusRing(
377
377
  onActivate: widget.onTap,
378
- borderRadius: BorderRadius.circular(999),
378
+ borderRadius: BorderRadius.circular(KasyRadius.full),
379
379
  child: GestureDetector(
380
380
  onTap: widget.onTap,
381
381
  behavior: HitTestBehavior.opaque,
@@ -21,7 +21,14 @@ class ReminderNotifier extends _$ReminderNotifier {
21
21
 
22
22
  Future<void> setType(ReminderType type) async {
23
23
  final current = state.requireValue;
24
- await _applyAndSave(current.copyWith(type: type));
24
+ // Leaving "specific date" drops the stored one-off date so it doesn't
25
+ // linger (and silently come back) if the user returns to that mode later.
26
+ await _applyAndSave(
27
+ current.copyWith(
28
+ type: type,
29
+ clearDate: type != ReminderType.specificDate,
30
+ ),
31
+ );
25
32
  }
26
33
 
27
34
  Future<void> setTime(int hour, int minute) async {
@@ -1,11 +1,14 @@
1
1
  import 'package:flutter/material.dart';
2
2
  import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+ import 'package:intl/intl.dart';
3
4
  import 'package:kasy_kit/components/kasy_app_bar.dart';
5
+ import 'package:kasy_kit/components/kasy_bottom_sheet.dart';
4
6
  import 'package:kasy_kit/components/kasy_card.dart';
5
7
  import 'package:kasy_kit/components/kasy_chip.dart';
6
8
  import 'package:kasy_kit/components/kasy_date_picker.dart';
7
9
  import 'package:kasy_kit/components/kasy_tabs.dart';
8
10
  import 'package:kasy_kit/core/theme/theme.dart';
11
+ import 'package:kasy_kit/core/widgets/kasy_hover.dart';
9
12
  import 'package:kasy_kit/core/widgets/kasy_scroll_behavior.dart';
10
13
  import 'package:kasy_kit/features/local_reminders/providers/reminder_notifier.dart';
11
14
  import 'package:kasy_kit/features/local_reminders/repositories/reminder_preferences.dart';
@@ -76,14 +79,16 @@ class _ReminderForm extends ConsumerWidget {
76
79
  child: Column(
77
80
  crossAxisAlignment: CrossAxisAlignment.stretch,
78
81
  children: [
82
+ // The master toggle reads like a Settings row: a colored icon
83
+ // badge, the label, and a live one-line summary of when the
84
+ // reminder fires (or a hint when it's off) as the subtitle.
79
85
  KasyCard(
80
- padding: const EdgeInsets.symmetric(
81
- horizontal: KasySpacing.md,
82
- vertical: KasySpacing.xs,
83
- ),
86
+ borderRadius: KasyRadius.lgBorderRadius,
84
87
  child: SettingsSwitchTile(
85
88
  icon: KasyIcons.notification,
89
+ iconBackgroundColor: context.colors.primary,
86
90
  title: tr.toggleLabel,
91
+ subtitle: _scheduleSummary(context, state),
87
92
  value: state.enabled,
88
93
  onChanged: notifier.setEnabled,
89
94
  ),
@@ -92,26 +97,17 @@ class _ReminderForm extends ConsumerWidget {
92
97
  const SizedBox(height: KasySpacing.xl),
93
98
  _FieldLabel(tr.typeLabel),
94
99
  const SizedBox(height: KasySpacing.sm),
95
- // Default hug mode scrolls horizontally when the labels don't
96
- // fit (long localized strings) instead of overflowing the row.
100
+ // Fill mode so the three options split the width equally
101
+ // (a segmented control), instead of hug mode where each tab
102
+ // only takes its own text width and the spacing looks uneven.
97
103
  KasyTabs(
98
104
  tabs: [tr.daily, tr.weekly, tr.specificDate],
99
105
  selectedIndex: _types.indexOf(state.type),
100
106
  onTabSelected: (i) => notifier.setType(_types[i]),
107
+ mode: KasyTabsMode.fill,
101
108
  ),
102
- // daily / weekly schedule by a wall-clock time (hour + minute);
103
- // specificDate carries its own time inside the chosen date.
104
- if (state.type == ReminderType.daily ||
105
- state.type == ReminderType.weekly) ...[
106
- const SizedBox(height: KasySpacing.lg),
107
- _FieldLabel(tr.timeLabel),
108
- const SizedBox(height: KasySpacing.sm),
109
- _TimeTile(
110
- hour: state.hour,
111
- minute: state.minute,
112
- onChanged: (h, m) => notifier.setTime(h, m),
113
- ),
114
- ],
109
+ // Pick the day/date BEFORE the time so the schedule reads in a
110
+ // natural order (which day at what time).
115
111
  if (state.type == ReminderType.weekly) ...[
116
112
  const SizedBox(height: KasySpacing.lg),
117
113
  _FieldLabel(tr.dayLabel),
@@ -126,6 +122,12 @@ class _ReminderForm extends ConsumerWidget {
126
122
  _FieldLabel(tr.dateLabel),
127
123
  const SizedBox(height: KasySpacing.sm),
128
124
  KasyDatePicker(
125
+ // Anchored popover on phones/tablets (the original feel),
126
+ // centered dialog on desktop. The call site picks by width
127
+ // via kasySheetUsesDialog so each presentation stays pure.
128
+ presentation: kasySheetUsesDialog(context)
129
+ ? KasyDatePickerPresentation.dialog
130
+ : KasyDatePickerPresentation.popover,
129
131
  value: state.date,
130
132
  minDate: DateTime.now(),
131
133
  onChanged: (picked) {
@@ -142,9 +144,14 @@ class _ReminderForm extends ConsumerWidget {
142
144
  );
143
145
  },
144
146
  ),
145
- const SizedBox(height: KasySpacing.lg),
146
- _FieldLabel(tr.timeLabel),
147
- const SizedBox(height: KasySpacing.sm),
147
+ ],
148
+ // Time is shared by every type. daily / weekly schedule by a
149
+ // wall-clock time (hour + minute); specificDate carries its own
150
+ // time inside the chosen date.
151
+ const SizedBox(height: KasySpacing.lg),
152
+ _FieldLabel(tr.timeLabel),
153
+ const SizedBox(height: KasySpacing.sm),
154
+ if (state.type == ReminderType.specificDate)
148
155
  _TimeTile(
149
156
  hour: state.date?.hour ?? 9,
150
157
  minute: state.date?.minute ?? 0,
@@ -155,8 +162,13 @@ class _ReminderForm extends ConsumerWidget {
155
162
  DateTime(base.year, base.month, base.day, h, m),
156
163
  );
157
164
  },
165
+ )
166
+ else
167
+ _TimeTile(
168
+ hour: state.hour,
169
+ minute: state.minute,
170
+ onChanged: (h, m) => notifier.setTime(h, m),
158
171
  ),
159
- ],
160
172
  ],
161
173
  ],
162
174
  ),
@@ -166,8 +178,43 @@ class _ReminderForm extends ConsumerWidget {
166
178
  }
167
179
  }
168
180
 
181
+ /// Human-readable, one-line description of the active schedule used as the
182
+ /// toggle subtitle. Reads "Every day at 09:00" / "Monday at 09:00" /
183
+ /// "On June 15 at 09:00", and falls back to a hint while the reminder is off
184
+ /// (or asks for a date when a specific-date reminder has none yet).
185
+ String _scheduleSummary(BuildContext context, ReminderState state) {
186
+ final tr = Translations.of(context).reminderPage;
187
+ if (!state.enabled) return tr.hint;
188
+
189
+ final String locale = Localizations.localeOf(context).toString();
190
+ String pad(int v) => v.toString().padLeft(2, '0');
191
+
192
+ switch (state.type) {
193
+ case ReminderType.daily:
194
+ return tr.summaryDaily(time: '${pad(state.hour)}:${pad(state.minute)}');
195
+ case ReminderType.weekly:
196
+ // January 2024 starts on a Monday, so day-of-month N maps to weekday N.
197
+ // DateFormat already cases the weekday per locale (e.g. "Tuesday" vs the
198
+ // lowercase "terça-feira"), so we use it as-is inside the summary.
199
+ final String day =
200
+ DateFormat.EEEE(locale).format(DateTime(2024, 1, state.dayOfWeek));
201
+ return tr.summaryWeekly(
202
+ day: day,
203
+ time: '${pad(state.hour)}:${pad(state.minute)}',
204
+ );
205
+ case ReminderType.specificDate:
206
+ final DateTime? date = state.date;
207
+ if (date == null) return tr.selectDate;
208
+ return tr.summaryDate(
209
+ date: DateFormat.MMMMd(locale).format(date),
210
+ time: '${pad(date.hour)}:${pad(date.minute)}',
211
+ );
212
+ }
213
+ }
214
+
169
215
  /// Quiet group eyebrow above each field — the design-system section label
170
216
  /// (small, gently tracked, muted), matching the Settings / sidebar pattern.
217
+ /// The small left inset aligns it with the Settings section labels.
171
218
  class _FieldLabel extends StatelessWidget {
172
219
  final String label;
173
220
 
@@ -175,10 +222,13 @@ class _FieldLabel extends StatelessWidget {
175
222
 
176
223
  @override
177
224
  Widget build(BuildContext context) {
178
- return Text(
179
- label,
180
- style: context.kasyTextTheme.sectionLabel.copyWith(
181
- color: context.colors.muted,
225
+ return Padding(
226
+ padding: const EdgeInsets.only(left: KasySpacing.xs),
227
+ child: Text(
228
+ label,
229
+ style: context.kasyTextTheme.sectionLabel.copyWith(
230
+ color: context.colors.muted,
231
+ ),
182
232
  ),
183
233
  );
184
234
  }
@@ -192,15 +242,21 @@ class _DaySelector extends StatelessWidget {
192
242
 
193
243
  @override
194
244
  Widget build(BuildContext context) {
195
- // 1=Monday ... 7=Sunday (matches DateTime.weekday)
196
- final days = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom'];
245
+ // 1=Monday ... 7=Sunday (matches DateTime.weekday). Short weekday names are
246
+ // localized via intl, so the chips read correctly in every app language
247
+ // instead of being hardcoded to one locale.
248
+ final DateFormat format = DateFormat.E(
249
+ Localizations.localeOf(context).toString(),
250
+ );
251
+ // January 2024 starts on a Monday, so day-of-month N maps to weekday N (1..7).
252
+ String shortName(int weekday) => format.format(DateTime(2024, 1, weekday));
197
253
  return Wrap(
198
254
  spacing: KasySpacing.xs,
199
255
  runSpacing: KasySpacing.xs,
200
256
  children: List.generate(7, (i) {
201
257
  final dayIndex = i + 1;
202
258
  return KasyChip(
203
- label: days[i],
259
+ label: shortName(dayIndex),
204
260
  selected: current == dayIndex,
205
261
  onSelected: (_) => onChanged(dayIndex),
206
262
  );
@@ -236,35 +292,40 @@ class _TimeTile extends StatelessWidget {
236
292
  }
237
293
  }
238
294
 
295
+ // Same surface + interaction model as a Settings row: an elevated card,
296
+ // a colored icon badge, the value, and a trailing chevron. KasyHover gives
297
+ // the press depth and keyboard focus ring, clipped to the card corners.
239
298
  return KasyCard(
240
- onTap: pickTime,
241
- borderRadius: KasyRadius.mdBorderRadius,
242
- padding: const EdgeInsets.symmetric(
243
- horizontal: KasySpacing.md,
244
- vertical: KasySpacing.smd,
245
- ),
246
- child: Row(
247
- children: [
248
- Icon(
249
- KasyIcons.time,
250
- size: KasyIconSize.lg,
251
- color: context.colors.primary,
252
- ),
253
- const SizedBox(width: KasySpacing.sm),
254
- Text(
255
- '${_pad(hour)}:${_pad(minute)}',
256
- style: context.textTheme.bodyLarge?.copyWith(
257
- color: context.colors.onSurface,
258
- fontWeight: FontWeight.w600,
299
+ borderRadius: KasyRadius.lgBorderRadius,
300
+ child: KasyHover(
301
+ onTap: pickTime,
302
+ focusable: true,
303
+ borderRadius: KasyRadius.lgBorderRadius,
304
+ focusGapColor: context.colors.surface,
305
+ semanticLabel: '${_pad(hour)}:${_pad(minute)}',
306
+ padding: const EdgeInsets.symmetric(
307
+ horizontal: KasySpacing.md,
308
+ vertical: KasySpacing.smd,
309
+ ),
310
+ child: Row(
311
+ children: [
312
+ SettingsIconBadge(
313
+ icon: KasyIcons.time,
314
+ color: context.colors.primary,
259
315
  ),
260
- ),
261
- const Spacer(),
262
- Icon(
263
- KasyIcons.arrowForwardIos,
264
- size: KasyIconSize.xs,
265
- color: context.colors.muted,
266
- ),
267
- ],
316
+ const SizedBox(width: KasySpacing.sm),
317
+ Expanded(
318
+ child: Text(
319
+ '${_pad(hour)}:${_pad(minute)}',
320
+ style: context.kasyTextTheme.listRowTitle.copyWith(
321
+ color: context.colors.onSurface,
322
+ fontWeight: FontWeight.w600,
323
+ ),
324
+ ),
325
+ ),
326
+ const SettingsListChevron(),
327
+ ],
328
+ ),
268
329
  ),
269
330
  );
270
331
  }