kasy-cli 1.38.0 → 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 (102) hide show
  1. package/lib/scaffold/CHANGELOG.json +14 -0
  2. package/lib/scaffold/backends/api/patch/README.md +15 -0
  3. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +11 -6
  4. package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +9 -1
  5. package/lib/scaffold/backends/api/pubspec.yaml.tpl +1 -0
  6. package/lib/scaffold/backends/patch-base-hashes.json +6 -6
  7. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +3 -1
  8. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +3 -0
  9. package/lib/scaffold/backends/supabase/migrations/20240101000012_welcome_decouple_from_push.sql +62 -0
  10. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +11 -6
  11. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  12. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +1 -0
  13. package/lib/scaffold/shared/generator-utils.js +12 -6
  14. package/package.json +1 -1
  15. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
  16. package/templates/firebase/DESIGN_SYSTEM.md +22 -8
  17. package/templates/firebase/assets/icons/apple_black.svg +3 -0
  18. package/templates/firebase/assets/icons/apple_white.svg +4 -0
  19. package/templates/firebase/assets/icons/facebook.svg +49 -0
  20. package/templates/firebase/assets/icons/google.svg +1 -0
  21. package/templates/firebase/functions/src/admin/functions.ts +2 -0
  22. package/templates/firebase/functions/src/authentication/functions.ts +13 -7
  23. package/templates/firebase/functions/src/notifications/triggers.ts +6 -2
  24. package/templates/firebase/lib/components/components.dart +1 -1
  25. package/templates/firebase/lib/components/kasy_app_bar.dart +314 -14
  26. package/templates/firebase/lib/components/kasy_card.dart +4 -0
  27. package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
  28. package/templates/firebase/lib/components/kasy_sidebar.dart +18 -6
  29. package/templates/firebase/lib/components/kasy_tabs.dart +31 -10
  30. package/templates/firebase/lib/components/kasy_text_field.dart +29 -7
  31. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +27 -18
  32. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +12 -10
  33. package/templates/firebase/lib/core/chrome/app_bar_config.dart +214 -0
  34. package/templates/firebase/lib/core/chrome/app_bar_scope.dart +102 -0
  35. package/templates/firebase/lib/core/data/api/user_api.dart +11 -0
  36. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
  37. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +18 -35
  38. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +11 -0
  39. package/templates/firebase/lib/core/states/logout_action.dart +11 -1
  40. package/templates/firebase/lib/core/states/user_state_notifier.dart +28 -1
  41. package/templates/firebase/lib/core/theme/texts.dart +21 -6
  42. package/templates/firebase/lib/core/theme/type_scale.dart +34 -15
  43. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +51 -19
  44. package/templates/firebase/lib/core/web_viewport_scale.dart +64 -35
  45. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +14 -3
  46. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +1 -1
  47. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +52 -35
  48. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
  49. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +18 -8
  50. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -61
  51. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +11 -61
  52. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +7 -5
  53. package/templates/firebase/lib/features/authentication/ui/widgets/social_auth_tile.dart +83 -0
  54. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +4 -4
  55. package/templates/firebase/lib/features/home/home_components_page.dart +253 -125
  56. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +231 -57
  57. package/templates/firebase/lib/features/home/home_feed.dart +2 -2
  58. package/templates/firebase/lib/features/home/home_image_grid.dart +3 -3
  59. package/templates/firebase/lib/features/local_reminders/providers/reminder_notifier.dart +8 -1
  60. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +111 -57
  61. package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
  62. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -4
  63. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +7 -56
  64. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +5 -5
  65. package/templates/firebase/lib/features/notifications/ui/widgets/push_permission_banner.dart +163 -0
  66. package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +2 -2
  67. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +9 -0
  68. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +28 -0
  69. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +51 -12
  70. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +90 -32
  71. package/templates/firebase/lib/features/settings/settings_page.dart +53 -32
  72. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +895 -111
  73. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  74. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +445 -233
  75. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +171 -41
  76. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +1 -1
  77. package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +9 -1
  78. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +48 -47
  79. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +21 -18
  80. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +25 -10
  81. package/templates/firebase/lib/i18n/en.i18n.json +749 -712
  82. package/templates/firebase/lib/i18n/es.i18n.json +749 -712
  83. package/templates/firebase/lib/i18n/pt.i18n.json +749 -712
  84. package/templates/firebase/lib/main.dart +20 -7
  85. package/templates/firebase/lib/router.dart +32 -26
  86. package/templates/firebase/pubspec.yaml +2 -1
  87. package/templates/firebase/test/admin_shell_chrome_test.dart +11 -5
  88. package/templates/firebase/test/components/kasy_text_field_height_test.dart +77 -0
  89. package/templates/firebase/test/core/web_viewport_scale_test.dart +23 -16
  90. package/templates/firebase/tool/design_check.dart +9 -0
  91. package/templates/firebase/assets/icons/apple.png +0 -0
  92. package/templates/firebase/assets/icons/facebook.png +0 -0
  93. package/templates/firebase/assets/icons/google.png +0 -0
  94. package/templates/firebase/assets/icons/google_play_games.png +0 -0
  95. package/templates/firebase/lib/components/kasy_web_header.dart +0 -218
  96. package/templates/firebase/lib/core/chrome/web_header_scope.dart +0 -20
  97. package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +0 -19
  98. package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +0 -32
  99. package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +0 -73
  100. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -66
  101. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +0 -179
  102. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +0 -106
@@ -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,5 +1,6 @@
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';
4
5
  import 'package:kasy_kit/components/kasy_bottom_sheet.dart';
5
6
  import 'package:kasy_kit/components/kasy_card.dart';
@@ -7,6 +8,7 @@ import 'package:kasy_kit/components/kasy_chip.dart';
7
8
  import 'package:kasy_kit/components/kasy_date_picker.dart';
8
9
  import 'package:kasy_kit/components/kasy_tabs.dart';
9
10
  import 'package:kasy_kit/core/theme/theme.dart';
11
+ import 'package:kasy_kit/core/widgets/kasy_hover.dart';
10
12
  import 'package:kasy_kit/core/widgets/kasy_scroll_behavior.dart';
11
13
  import 'package:kasy_kit/features/local_reminders/providers/reminder_notifier.dart';
12
14
  import 'package:kasy_kit/features/local_reminders/repositories/reminder_preferences.dart';
@@ -77,14 +79,16 @@ class _ReminderForm extends ConsumerWidget {
77
79
  child: Column(
78
80
  crossAxisAlignment: CrossAxisAlignment.stretch,
79
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.
80
85
  KasyCard(
81
- padding: const EdgeInsets.symmetric(
82
- horizontal: KasySpacing.md,
83
- vertical: KasySpacing.xs,
84
- ),
86
+ borderRadius: KasyRadius.lgBorderRadius,
85
87
  child: SettingsSwitchTile(
86
88
  icon: KasyIcons.notification,
89
+ iconBackgroundColor: context.colors.primary,
87
90
  title: tr.toggleLabel,
91
+ subtitle: _scheduleSummary(context, state),
88
92
  value: state.enabled,
89
93
  onChanged: notifier.setEnabled,
90
94
  ),
@@ -93,26 +97,17 @@ class _ReminderForm extends ConsumerWidget {
93
97
  const SizedBox(height: KasySpacing.xl),
94
98
  _FieldLabel(tr.typeLabel),
95
99
  const SizedBox(height: KasySpacing.sm),
96
- // Default hug mode scrolls horizontally when the labels don't
97
- // 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.
98
103
  KasyTabs(
99
104
  tabs: [tr.daily, tr.weekly, tr.specificDate],
100
105
  selectedIndex: _types.indexOf(state.type),
101
106
  onTabSelected: (i) => notifier.setType(_types[i]),
107
+ mode: KasyTabsMode.fill,
102
108
  ),
103
- // daily / weekly schedule by a wall-clock time (hour + minute);
104
- // specificDate carries its own time inside the chosen date.
105
- if (state.type == ReminderType.daily ||
106
- state.type == ReminderType.weekly) ...[
107
- const SizedBox(height: KasySpacing.lg),
108
- _FieldLabel(tr.timeLabel),
109
- const SizedBox(height: KasySpacing.sm),
110
- _TimeTile(
111
- hour: state.hour,
112
- minute: state.minute,
113
- onChanged: (h, m) => notifier.setTime(h, m),
114
- ),
115
- ],
109
+ // Pick the day/date BEFORE the time so the schedule reads in a
110
+ // natural order (which day at what time).
116
111
  if (state.type == ReminderType.weekly) ...[
117
112
  const SizedBox(height: KasySpacing.lg),
118
113
  _FieldLabel(tr.dayLabel),
@@ -149,9 +144,14 @@ class _ReminderForm extends ConsumerWidget {
149
144
  );
150
145
  },
151
146
  ),
152
- const SizedBox(height: KasySpacing.lg),
153
- _FieldLabel(tr.timeLabel),
154
- 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)
155
155
  _TimeTile(
156
156
  hour: state.date?.hour ?? 9,
157
157
  minute: state.date?.minute ?? 0,
@@ -162,8 +162,13 @@ class _ReminderForm extends ConsumerWidget {
162
162
  DateTime(base.year, base.month, base.day, h, m),
163
163
  );
164
164
  },
165
+ )
166
+ else
167
+ _TimeTile(
168
+ hour: state.hour,
169
+ minute: state.minute,
170
+ onChanged: (h, m) => notifier.setTime(h, m),
165
171
  ),
166
- ],
167
172
  ],
168
173
  ],
169
174
  ),
@@ -173,8 +178,43 @@ class _ReminderForm extends ConsumerWidget {
173
178
  }
174
179
  }
175
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
+
176
215
  /// Quiet group eyebrow above each field — the design-system section label
177
216
  /// (small, gently tracked, muted), matching the Settings / sidebar pattern.
217
+ /// The small left inset aligns it with the Settings section labels.
178
218
  class _FieldLabel extends StatelessWidget {
179
219
  final String label;
180
220
 
@@ -182,10 +222,13 @@ class _FieldLabel extends StatelessWidget {
182
222
 
183
223
  @override
184
224
  Widget build(BuildContext context) {
185
- return Text(
186
- label,
187
- style: context.kasyTextTheme.sectionLabel.copyWith(
188
- 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
+ ),
189
232
  ),
190
233
  );
191
234
  }
@@ -199,15 +242,21 @@ class _DaySelector extends StatelessWidget {
199
242
 
200
243
  @override
201
244
  Widget build(BuildContext context) {
202
- // 1=Monday ... 7=Sunday (matches DateTime.weekday)
203
- 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));
204
253
  return Wrap(
205
254
  spacing: KasySpacing.xs,
206
255
  runSpacing: KasySpacing.xs,
207
256
  children: List.generate(7, (i) {
208
257
  final dayIndex = i + 1;
209
258
  return KasyChip(
210
- label: days[i],
259
+ label: shortName(dayIndex),
211
260
  selected: current == dayIndex,
212
261
  onSelected: (_) => onChanged(dayIndex),
213
262
  );
@@ -243,35 +292,40 @@ class _TimeTile extends StatelessWidget {
243
292
  }
244
293
  }
245
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.
246
298
  return KasyCard(
247
- onTap: pickTime,
248
- borderRadius: KasyRadius.mdBorderRadius,
249
- padding: const EdgeInsets.symmetric(
250
- horizontal: KasySpacing.md,
251
- vertical: KasySpacing.smd,
252
- ),
253
- child: Row(
254
- children: [
255
- Icon(
256
- KasyIcons.time,
257
- size: KasyIconSize.lg,
258
- color: context.colors.primary,
259
- ),
260
- const SizedBox(width: KasySpacing.sm),
261
- Text(
262
- '${_pad(hour)}:${_pad(minute)}',
263
- style: context.textTheme.bodyLarge?.copyWith(
264
- color: context.colors.onSurface,
265
- 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,
266
315
  ),
267
- ),
268
- const Spacer(),
269
- Icon(
270
- KasyIcons.arrowForwardIos,
271
- size: KasyIconSize.xs,
272
- color: context.colors.muted,
273
- ),
274
- ],
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
+ ),
275
329
  ),
276
330
  );
277
331
  }